QQ主页抽屉效果实现,有趣的d簧动效

QQ主页抽屉效果实现,有趣的d簧动效,第1张

QQ主页抽屉效果实现,有趣的d簧动效

最近在测试机玩QQ的时候,留意到QQ主页上抽屉打开时,主页有一个类似d簧的动效,觉得挺有意思,今天就来实现以下QQ主页的抽屉动效。

先来看看我看到的QQ主页抽屉动画是如何的:

抽屉打开的时候,可以看到主页是有两个动作:

    主界面收缩主界面四周圆角度数变大

接下来就一步步实现QQ抽屉打开时的效果。

我复现的最终效果:

复现第一步,整一个沉浸式状态栏,状态栏有个头像可以打开抽屉

新建个 Activity,命名为 DrawerActivity:

class DrawerActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "DrawerActivity"
    }

    private lateinit var binding: ActivityDrawerBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityDrawerBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

用 ViewBinding 绑定布局文件,接下来编辑 Activity 的布局文件:


    
    

        

        

            
        
    

    
    


布局文件就不过多说明了,就是简简单单,一个主页的壳子。

不过目前 Activity 自带的 ActionBar 还在,我们需要把它去掉

 

设置完这个属性,重启 Activity 会发现状态栏是灰色的,因此需要将状态栏的颜色改为透明,实现一个沉浸式状态栏的效果。向 Activity 中添加如下代码:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityDrawer2Binding.inflate(layoutInflater)
    setContentView(binding.root)

    setStatusBar()
}

private fun setStatusBar() {
    // 利用状态栏工具类
    StatusBarUtil.fitStatusLayout(this, binding.toolbar, true)
}

这里利用了一个StatusBarUtil状态栏工具类来设置沉浸式状态栏,代码文件可以在这个地址获取。

看下沉浸式状态栏设置后的效果:

下一步给标题栏左部整个头像,实现点击头像可以打开抽屉的效果:

向 Activity 中添加如下代码:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityDrawer2Binding.inflate(layoutInflater)
    setContentView(binding.root)

    setStatusBar()

    binding.toolbar.setNavigationIcon(R.drawable.icon_head)
    binding.toolbar.setNavigationOnClickListener {
        binding.drawerLayout.openDrawer(Gravity.LEFT, true)
    }
}

好的,抽屉打开了,但是可以看到,抽屉打开后,抽屉的右部离屏幕右边还有一小段间隙,我们希望抽屉打开后可以占满全屏。虽然我们在布局文件已经给 NavigationView设置了 match_parent 的宽度,但DrawerLayout默认会给抽屉留有 65dp 的间隙空间,我们可以给 NavigationView 设置 -65dp 的右边距,让抽屉打开时占满全屏:


这样抽屉打开时就可以占满全屏了。

复现第二步,实现抽屉打开时,主界面的缩放

这个动画效果,是在抽屉打开的过程中执行,因此我们需要监听抽屉打开的事件

DrawerLayout 提供了添加监听的 API:

public void addDrawerListener(@NonNull DrawerListener listener) {
    if (listener == null) {
        return;
    }
    if (mListeners == null) {
        mListeners = new ArrayList();
    }
    mListeners.add(listener);
}

addDrawerListener() 接受一个 DrawerListener 对象,这个 Listener 是一个接口,其内部定义了 4 个方法。

方法描述补充void onDrawerSlide(@NonNull View drawerView, float slideOffset)在抽屉滑动的过程中回调,滑动的偏移量会返回给 slideOffset,其值在 0 到 1 范围之间变化,当抽屉完全打开时 offset 值为 1,完全关闭时 offset 值为0。这个回调方法会随着抽屉的移动,被回调多次。void onDrawerOpened(@NonNull View drawerView)抽屉完全打开时回调,在抽屉打开或关闭的过程中仅会回调一次。void onDrawerOpened(@NonNull View drawerView)抽屉完全关闭时回调,在抽屉打开或关闭的过程中仅会回调一次。void onDrawerStateChanged(@State int newState)抽屉状态发生变化时滑动,包含三个状态。 该回调也会被调用多次,如果抽屉是由用户拖动打开的,那么一次拖动到结束的过程,抽屉会顺序经历:DRAGGING, SETTLING, IDEL状态,如果抽屉的打开不经过用户的拖动,而是直接通过调用 DrawerLayout.openDrawer()方法打开的话,则只会顺序经历:SETTLING, IDEL状态,没有拖动状态。1. STATE_IDEL : 表抽屉此时处于IDEL状态,没有任何动画正在执行。 2. STATE_DRAGGING:表抽屉当前正在被用户拖动状态。 3. STATE_SETTLING:表抽屉正处于滑动到最后位置的过程中,最后位置,可能是关闭位置,也可能是打开位置。

这里我们需要监听抽屉滑动时的事件,所以需要构建一个 DrawerListener,实现其 onDrawerSlide() 方法

在 Activity 中定义一个方法,名叫 setDrawerLayout。

在抽屉滑动时,让主界面整个布局缩小,可以在包含主界面的 ViewGoup 上添加一定外边距,并设置根布局的背景颜色为黑色即可。

private fun setDrawerLayout() {
  // 根据屏幕宽高比例,向主界面添加外边距
  val factor = getScreenWidth(this) * 1f / getScreenHeight(this)
  val topMargin = 36.dp2px(this)
  val leftMargin = (topMargin + 6 ) * factor

  binding.drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {

      JLog.d(TAG, "offset = $slideOffset")
      // 滑动过程中,根据 offset 的值,更新上下左右的 margin 值
      val layoutParams = binding.mainContainer.layoutParams as? DrawerLayout.LayoutParams
      layoutParams?.let {
        it.topMargin = (topMargin * slideOffset).roundToInt()
        it.bottomMargin = (topMargin * slideOffset).roundToInt()
        it.leftMargin = (leftMargin * slideOffset).roundToInt()
        it.rightMargin = (leftMargin * slideOffset).roundToInt()
      }
      binding.mainContainer.layoutParams = layoutParams
    }
  })
}

getScreenWidth(),getScreenHeight(),和 Int.dp2px() 方法,是直接调用工具类的方法,工具类文件可以从这里 WidgetExtension.kt 获得。

接着设置主界面根布局背景色为黑色。

		...

看看效果:

可以看到,主界面随着抽屉滑动收缩的动效已经做好了。

复现第三步,实现主界面四周圆角度数随着滑动过程变化

这一步其实也简单,就是要给主界面布局的四周设置一个圆角。

我们经常通过定义一个 drawable 文件,给 shape 设置 corners 属性,来给控件定义圆角:




    
    


但这种方法只能给布局文件设置一个静态的圆角,圆角度数在界面显示的时候定死了。而我们希望主界面四周圆角在抽屉滑动的时候,动态更新,因此不能通过布局文件的方式。

那么如何给控件添加一个动态的圆角呢?答案是使用 GradientDrawable

GradientDrawable 支持设置颜色的渐变,边框,圆角等属性,用起来与在布局文件上定义一个 shape drawable 是类似的。与之不同的是,我们可以通过 代码,将 gradient drawable 动态地设置到布局当中。

public void setCornerRadii(@Nullable float[] radii) {
    mGradientState.setCornerRadii(radii);
    mPathIsDirty = true;
    invalidateSelf();
}

接下来补充 setDrawerLayout() 方法:

private fun setDrawerLayout() {
    val factor = getScreenWidth(this) * 1f / getScreenHeight(this)
    val topMargin = 36.dp2px(this)
    val leftMargin = (topMargin + 6 ) * factor

	// 定义四周圆角度数
    val radius = 20.dp2px(this)
    val toolbarDrawable = GradientDrawable()
    toolbarDrawable.shape = GradientDrawable.RECTANGLE
    toolbarDrawable.setColor(Color.parseColor("#1ABDE6"))
    binding.toolbar.background = toolbarDrawable

    val contentDrawable = GradientDrawable()
    contentDrawable.shape = GradientDrawable.RECTANGLE
    contentDrawable.setColor(Color.WHITE)
    binding.contentContainer.background = contentDrawable

    binding.drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
        override fun onDrawerSlide(drawerView: View, slideOffset: Float) {

            JLog.d(TAG, "offset = $slideOffset")

          	// 设置主界面 toolbar 的左上和右上部分圆角
            val windowRadius = radius * slideOffset
            val tDb = binding.toolbar.background as? GradientDrawable
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
              	// 根据其 API 定义,只设置前4个数值
                tDb?.cornerRadii = floatArrayOf(windowRadius, windowRadius, windowRadius, windowRadius, 0f, 0f, 0f, 0f)
                binding.toolbar.background = tDb
            }

            val cDb = binding.contentContainer.background as? GradientDrawable
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
              	// // 根据其 API 定义,只设置后4个数值
                cDb?.cornerRadii = floatArrayOf(0f, 0f, 0f, 0f, windowRadius, windowRadius, windowRadius, windowRadius)
                binding.contentContainer.background = cDb
            }

          	// 设置主界面主页内容 的左下和右下部分圆角
            val layoutParams = binding.mainContainer.layoutParams as? CustomDrawerLayout.LayoutParams
            layoutParams?.let {
                it.topMargin = (topMargin * slideOffset).roundToInt()
                it.bottomMargin = (topMargin * slideOffset).roundToInt()
                it.leftMargin = (leftMargin * slideOffset).roundToInt()
                it.rightMargin = (leftMargin * slideOffset).roundToInt()
            }
            binding.mainContainer.layoutParams = layoutParams
        }
    })
}

一顿 *** 作后,一个 QQ 版的抽屉动效就实现好了:

进一步优化 1. 延长抽屉动效执行时长,让抽屉动效执行更流畅。

基本上已经实现好了,不过可以发现,当我们点击标题栏的头像,打开抽屉时,抽屉会大约在0.5s内打开,这时候主界面的缩放和圆角动效就变得不明显了,用户难以捕捉到,而且因为抽屉滑动迅速,主界面收缩时可能还会出现卡顿、掉帧的情况。

通过对比 QQ 的抽屉动效发现,QQ 抽屉的打开和关闭时间是固定的,而且会比默认的抽屉打开和关闭所用的时间要久。也即 QQ 抽屉打开和关闭也做了降速处理,来提高用户的交互体验。

所以,我们实现的抽屉动效也需要做一定的降速,来提升用户体验。但是 DrawerLayout 并没有对外开放设置抽屉打开和关闭时长的 API,如何才能自定义一个抽屉动效执行时长呢?

我通过点进 DrawerLayout.openDrawer() 方法发现,其抽屉动效执行时长是根据 ViewDragHelper 类 中的 computeSettleDuration() 方法计算的。抽屉关闭和打开时,都会经过这个方法去计算抽屉动效的执行时长。

但很可惜,这个方法被定义成了类的私有方法,而且 ViewDragHelper 与 DrawerLayout 的耦合度太高,没有办法通过继承 ViewDragHelper 类,重写computeSettleDuration()方法的方式去自定义时长。

我这里介绍一个土方法,那就是直接将 DrawerLayout 和 ViewDragHelper 拷贝一份到自己的项目,然后重新定义 ViewDragHelper的 computeSettleDuration()方法,再将项目中使用的系统 DrawerLayout 替换成项目中定义的,就可以自定义抽屉的滑动时长了。

这是一个很土的方法,我目前也没有发现一个更好的方法了,所以这里凑活用下。

修改项目中 ViewDragHelper 的 computeSettleDuration() 方法:

private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
	return 680;
}

直接返回一个固定的时长,经测试,在我的项目中,680ms 是一个合适的时长,在这个时长下,我的抽屉动效执行非常流畅。大家可以根据自己项目主界面的布局情况,选择一个合理的时长。

2. 在抽屉打开时,若用户侧滑返回,或按键返回,应先关闭抽屉

在抽屉完全打开时,若用户按下返回键,系统默认会直接退出该 Activity,这种交互体验不太好,更多情况下,用户是希望在按下返回键时,关闭抽屉,所以需要在抽屉完全打开时屏蔽掉一次用户的返回键按下事件:

override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    if (keyCode == KeyEvent.KEYCODE_BACK && event?.repeatCount == 0) {
        // 抽屉如果是打开状态,先关闭抽屉
        if (binding.drawerLayout.isDrawerOpen(binding.navigationView)) {
            binding.drawerLayout.closeDrawer(Gravity.LEFT)
            return false
        }
    }
    return super.onKeyDown(keyCode, event)
}

好了,以上就是实现 QQ 版抽屉动效的全部内容,源码地址在这,需要的同学可以自取,希望对你有所帮助。

兄dei,如果觉得我写的还不错,麻烦帮个忙呗 
    给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)不用点收藏,诶别点啊,你怎么点了?这多不好意思!噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

拜托拜托,谢谢各位同学!

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5709374.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存