最近在测试机玩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 个方法。
这里我们需要监听抽屉滑动时的事件,所以需要构建一个 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,如果觉得我写的还不错,麻烦帮个忙呗
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)不用点收藏,诶别点啊,你怎么点了?这多不好意思!噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!
拜托拜托,谢谢各位同学!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)