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

相关文章

基于C++的UDP网络通信系统设计与实现详解

《基于C++的UDP网络通信系统设计与实现详解》在网络编程领域,UDP作为一种无连接的传输层协议,以其高效、低延迟的特性在实时性要求高的应用场景中占据重要地位,下面我们就来看看如何从零开始构建一个完整... 目录前言一、UDP服务器UdpServer.hpp1.1 基本框架设计1.2 初始化函数Init详解

Java中Map的五种遍历方式实现与对比

《Java中Map的五种遍历方式实现与对比》其实Map遍历藏着多种玩法,有的优雅简洁,有的性能拉满,今天咱们盘一盘这些进阶偏基础的遍历方式,告别重复又臃肿的代码,感兴趣的小伙伴可以了解下... 目录一、先搞懂:Map遍历的核心目标二、几种遍历方式的对比1. 传统EntrySet遍历(最通用)2. Lambd

springboot+redis实现订单过期(超时取消)功能的方法详解

《springboot+redis实现订单过期(超时取消)功能的方法详解》在SpringBoot中使用Redis实现订单过期(超时取消)功能,有多种成熟方案,本文为大家整理了几个详细方法,文中的示例代... 目录一、Redis键过期回调方案(推荐)1. 配置Redis监听器2. 监听键过期事件3. Redi

SpringBoot全局异常拦截与自定义错误页面实现过程解读

《SpringBoot全局异常拦截与自定义错误页面实现过程解读》本文介绍了SpringBoot中全局异常拦截与自定义错误页面的实现方法,包括异常的分类、SpringBoot默认异常处理机制、全局异常拦... 目录一、引言二、Spring Boot异常处理基础2.1 异常的分类2.2 Spring Boot默

基于SpringBoot实现分布式锁的三种方法

《基于SpringBoot实现分布式锁的三种方法》这篇文章主要为大家详细介绍了基于SpringBoot实现分布式锁的三种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、基于Redis原生命令实现分布式锁1. 基础版Redis分布式锁2. 可重入锁实现二、使用Redisso

SpringBoo WebFlux+MongoDB实现非阻塞API过程

《SpringBooWebFlux+MongoDB实现非阻塞API过程》本文介绍了如何使用SpringBootWebFlux和MongoDB实现非阻塞API,通过响应式编程提高系统的吞吐量和响应性能... 目录一、引言二、响应式编程基础2.1 响应式编程概念2.2 响应式编程的优势2.3 响应式编程相关技术

C#实现将XML数据自动化地写入Excel文件

《C#实现将XML数据自动化地写入Excel文件》在现代企业级应用中,数据处理与报表生成是核心环节,本文将深入探讨如何利用C#和一款优秀的库,将XML数据自动化地写入Excel文件,有需要的小伙伴可以... 目录理解XML数据结构与Excel的对应关系引入高效工具:使用Spire.XLS for .NETC

Nginx更新SSL证书的实现步骤

《Nginx更新SSL证书的实现步骤》本文主要介绍了Nginx更新SSL证书的实现步骤,包括下载新证书、备份旧证书、配置新证书、验证配置及遇到问题时的解决方法,感兴趣的了解一下... 目录1 下载最新的SSL证书文件2 备份旧的SSL证书文件3 配置新证书4 验证配置5 遇到的http://www.cppc

Nginx之https证书配置实现

《Nginx之https证书配置实现》本文主要介绍了Nginx之https证书配置的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起... 目录背景介绍为什么不能部署在 IIS 或 NAT 设备上?具体实现证书获取nginx配置扩展结果验证

SpringBoot整合 Quartz实现定时推送实战指南

《SpringBoot整合Quartz实现定时推送实战指南》文章介绍了SpringBoot中使用Quartz动态定时任务和任务持久化实现多条不确定结束时间并提前N分钟推送的方案,本文结合实例代码给大... 目录前言一、Quartz 是什么?1、核心定位:解决什么问题?2、Quartz 核心组件二、使用步骤1