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

2023-11-11 13:21

本文主要是介绍QQ主页抽屉效果实现,有趣的弹簧动效,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

1_qq主页抽屉动画演示.gif

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

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

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

我复现的最终效果:

2_复现结果演示.gif

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

新建个 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>

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

3_toolbar_实现.png

不过目前 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状态栏工具类来设置沉浸式状态栏,代码文件可以在这个地址获取。

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

4_沉浸式状态栏设置.png

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

向 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)}
}

5_抽屉打开演示.gif

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

<!-- Drawer -->
<com.google.android.material.navigation.NavigationView...android:layout_marginRight="-65dp"... />

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

6_抽屉全屏演示.gif

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

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

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>

看看效果:

7_主界面缩放演示.gif

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

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

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

我们经常通过定义一个 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 版的抽屉动效就实现好了:

8_结果演示.gif

进一步优化
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,如果觉得我写的还不错,麻烦帮个忙呗 😃
  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#.#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

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

这篇关于QQ主页抽屉效果实现,有趣的弹簧动效的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/390355

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

防近视护眼台灯什么牌子好?五款防近视效果好的护眼台灯推荐

在家里,灯具是属于离不开的家具,每个大大小小的地方都需要的照亮,所以一盏好灯是必不可少的,每个发挥着作用。而护眼台灯就起了一个保护眼睛,预防近视的作用。可以保护我们在学习,阅读的时候提供一个合适的光线环境,保护我们的眼睛。防近视护眼台灯什么牌子好?那我们怎么选择一个优秀的护眼台灯也是很重要,才能起到最大的护眼效果。下面五款防近视效果好的护眼台灯推荐: 一:六个推荐防近视效果好的护眼台灯的

nudepy,一个有趣的 Python 库!

更多资料获取 📚 个人网站:ipengtao.com 大家好,今天为大家分享一个有趣的 Python 库 - nudepy。 Github地址:https://github.com/hhatto/nude.py 在图像处理和计算机视觉应用中,检测图像中的不适当内容(例如裸露图像)是一个重要的任务。nudepy 是一个基于 Python 的库,专门用于检测图像中的不适当内容。该

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、