本文主要是介绍QQ主页抽屉效果实现,有趣的弹簧动效,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
最近在测试机玩QQ的时候,留意到QQ主页上抽屉打开时,主页有一个类似弹簧的动效,觉得挺有意思,今天就来实现以下QQ主页的抽屉动效。
先来看看我看到的QQ主页抽屉动画是如何的:
抽屉打开的时候,可以看到主页是有两个动作:
- 主界面收缩
- 主界面四周圆角度数变大
接下来就一步步实现QQ抽屉打开时的效果。
我复现的最终效果:
复现第一步,整一个沉浸式状态栏,状态栏有个头像可以打开抽屉
新建个 Activity,命名为 DrawerActivity:
class DrawerActivity : AppCompatActivity() {companion object {private const val TAG = "DrawerActivity"}private lateinit var binding: ActivityDrawerBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityDrawerBinding.inflate(layoutInflater)setContentView(binding.root)}
}
用 ViewBinding 绑定布局文件,接下来编辑 Activity 的布局文件:
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/drawerLayout"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#000000"tools:openDrawer="start"tools:context=".dawerlayout.DrawerActivity"><!-- Main Content --><androidx.appcompat.widget.LinearLayoutCompatandroid:id="@+id/mainContainer"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><androidx.appcompat.widget.Toolbarandroid:id="@+id/toolbar"android:layout_width="match_parent"android:layout_height="56dp"app:subtitleTextColor="#FFFFFF"android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"app:title="个人中心"app:titleTextColor="#FFFFFF" /><RelativeLayoutandroid:id="@+id/contentContainer"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#FFFFFF"><androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/tvContent"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="主页内容"android:textSize="16sp"android:textColor="#000000"android:layout_centerInParent="true" /></RelativeLayout></androidx.appcompat.widget.LinearLayoutCompat><!-- Drawer --><com.google.android.material.navigation.NavigationViewandroid:id="@+id/navigationView"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="start"android:background="#AFEEEF"app:headerLayout="@layout/navigationview_header"/></androidx.drawerlayout.widget.DrawerLayout>
布局文件就不过多说明了,就是简简单单,一个主页的壳子。
不过目前 Activity 自带的 ActionBar 还在,我们需要把它去掉
<activity android:name=".dawerlayout.DrawerActivity"android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
设置完这个属性,重启 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 的右边距,让抽屉打开时占满全屏:
<!-- Drawer -->
<com.google.android.material.navigation.NavigationView...android:layout_marginRight="-65dp"... />
这样抽屉打开时就可以占满全屏了。
复现第二步,实现抽屉打开时,主界面的缩放
这个动画效果,是在抽屉打开的过程中执行,因此我们需要监听抽屉打开的事件
DrawerLayout 提供了添加监听的 API:
/*** Adds the specified listener to the list of listeners that will be notified of drawer events.** @param listener Listener to notify when drawer events occur.* @see #removeDrawerListener(DrawerListener)*/
public void addDrawerListener(@NonNull DrawerListener listener) {if (listener == null) {return;}if (mListeners == null) {mListeners = new ArrayList<DrawerListener>();}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 ) * factorbinding.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.LayoutParamslayoutParams?.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 获得。
接着设置主界面根布局背景色为黑色。
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/drawerLayout"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#000000"tools:openDrawer="start"tools:context=".dawerlayout.DrawerActivity">...
</androidx.drawerlayout.widget.DrawerLayout>
看看效果:
可以看到,主界面随着抽屉滑动收缩的动效已经做好了。
复现第三步,实现主界面四周圆角度数随着滑动过程变化
这一步其实也简单,就是要给主界面布局的四周设置一个圆角。
我们经常通过定义一个 drawable 文件,给 shape 设置 corners 属性,来给控件定义圆角:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"android:shape="rectangle"><corners android:radius="10dp" /><solid android:color="#AFEEEA" /></shape>
但这种方法只能给布局文件设置一个静态的圆角,圆角度数在界面显示的时候定死了。而我们希望主界面四周圆角在抽屉滑动的时候,动态更新,因此不能通过布局文件的方式。
那么如何给控件添加一个动态的圆角呢?答案是使用 GradientDrawable
GradientDrawable 支持设置颜色的渐变,边框,圆角等属性,用起来与在布局文件上定义一个 shape drawable 是类似的。与之不同的是,我们可以通过 代码,将 gradient drawable 动态地设置到布局当中。
/**方法接受一个float数组,接受4对圆角数据,也即8个圆角数值圆角定义顺序为:左上X方向的圆角,左上Y方向的圆角,右上X方向的圆角,右上Y方向的圆角,右下X方向的圆角,右下Y方向的圆角,左下X方向的圆角,左下Y方向的圆角,
*/
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.RECTANGLEtoolbarDrawable.setColor(Color.parseColor("#1ABDE6"))binding.toolbar.background = toolbarDrawableval contentDrawable = GradientDrawable()contentDrawable.shape = GradientDrawable.RECTANGLEcontentDrawable.setColor(Color.WHITE)binding.contentContainer.background = contentDrawablebinding.drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {override fun onDrawerSlide(drawerView: View, slideOffset: Float) {JLog.d(TAG, "offset = $slideOffset")// 设置主界面 toolbar 的左上和右上部分圆角val windowRadius = radius * slideOffsetval tDb = binding.toolbar.background as? GradientDrawableif (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? GradientDrawableif (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.LayoutParamslayoutParams?.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,如果觉得我写的还不错,麻烦帮个忙呗 😃
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
- 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
- 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!
拜托拜托,谢谢各位同学!
这篇关于QQ主页抽屉效果实现,有趣的弹簧动效的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!