Android Compose衰减动画Animatable使用详解
作者:loongwind 发布时间:2022-08-26 23:09:42
前言
之前介绍了 Animatable
动画以及其 animateTo
和 snapTo
两个开启动画 api 的使用,实际上 Animatable
除了这两个 api 以外还有一个 animateDecay
即本篇要介绍的衰减动画。
什么是衰减动画呢?就是动画速度由快到慢最后停止,最常见的应用场景就是惯性动画,比如滑动列表时手指松开后列表不会立即停止而是会继续滑动一段距离后才停止;下面就来看看 animateDecay
具体如何使用。
animateDecay
首先还是来看一下 animateDecay
的定义:
suspend fun animateDecay(
initialVelocity: T,
animationSpec: DecayAnimationSpec<T>,
block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>
跟前面介绍的 animateTo
和 snapTo
一样都是 suspend
修饰的方法,即必须在协程中调用,参数有三个,分别解析如下:
initialVelocity:初始速度
animationSpec:动画配置,
DecayAnimationSpec
类型block:函数类型参数,动画运行的每一帧都会回调这个 block 方法,可用于动画监听
返回值跟 animateTo
一样都是 AnimationResult
类型。
initialVelocity
是动画的初始速度,动画会从这个初始速度按照一定的衰减曲线进行衰减,直到速度为 0 或达到阈值时动画停止。那这个初始速度的单位是多少呢?是单位/秒 这里的单位就是动画作用的数值类型,比如数值类型是 Dp,那就代表多少 Dp 每秒。
而衰减曲线的配置就是第二个参数 animationSpec
,需要注意的是这里的 animationSpec
是 DecayAnimationSpec
类型,它并不是前面介绍的 AnimationSpec
的子类,是衰减动画特有的动画配置,看一下 DecayAnimationSpec
的定义:
interface DecayAnimationSpec<T> {
fun <V : AnimationVector> vectorize(
typeConverter: TwoWayConverter<T, V>
): VectorizedDecayAnimationSpec<V>
}
从源码可以知晓,DecayAnimationSpec
是一个独立的接口,跟踪其实现类只有一个 DecayAnimationSpecImpl
:
private class DecayAnimationSpecImpl<T>(
private val floatDecaySpec: FloatDecayAnimationSpec
) : DecayAnimationSpec<T> {
override fun <V : AnimationVector> vectorize(
typeConverter: TwoWayConverter<T, V>
): VectorizedDecayAnimationSpec<V> = VectorizedFloatDecaySpec(floatDecaySpec)
}
这个实现类是 private
的,也就是不能直接创建其实例,那怎么创建呢?Compose 提供三个方法用于创建,分别是 splineBasedDecay
、rememberSplineBasedDecay
和 exponentialDecay
,那么这三种方法又有什么区别呢?下面分别对其进行详细介绍。
splineBasedDecay
splineBasedDecay
根据方法命名我们可以翻译为基于样条曲线的衰减,什么是样条曲线呢?Google得到的答案:样条曲线是经过或接近影响曲线形状的一系列点的平滑曲线。更抽象了,实际上我们并不需要了解他是怎么实现的,当然感兴趣的可以自行查询相关资料,我们只要知道在 Android 中默认的列表惯性滑动就是基于此曲线算法实现的。
概念了解清楚后,再来看一下 splineBasedDecay
方法的定义:
fun <T> splineBasedDecay(density: Density): DecayAnimationSpec<T>
只有一个参数 density
即屏幕像素密度。为什么要传 density
呢?这是因为 splineBasedDecay
是基于屏幕像素进行的动画速度衰减,当像素密度越大动画减速越快,动画的时长越短,动画惯性滑动的距离越短;可以理解屏幕像素密度越大摩擦力越大,所以惯性滑动的距离就越短。
使用 splineBasedDecay
实现动画效果,代码如下:
// 创建 Animatable 实例
val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }
val scope = rememberCoroutineScope()
// 创建 splineBasedDecay
// 通过 LocalDensity.current 获取当前设备屏幕密度
val splineBasedDecay = splineBasedDecay<Dp>(LocalDensity.current)
Box(
Modifier
.padding(start = 10.dp, top = animatable.value)
.size(100.dp, 100.dp)
.background(Color.Blue)
.clickable {
scope.launch {
// 启动衰减动画,初始速度设置为 1000.dp 每秒
animatable.animateDecay(1000.dp, splineBasedDecay)
}
}
)
将上述代码分别在屏幕尺寸均为 6.0 英寸、屏幕密度分别为 440 dpi 和 320 dpi 的设备上运行,效果如下:
可以发现,屏幕密度小的动画运行的距离更长。
rememberSplineBasedDecay
rememberSplineBasedDecay
跟 splineBasedDecay
的作用是一样的,区别在 splineBasedDecay
上用 remember
包裹了一层,上一节中使用 splineBasedDecay
并未用 remember
包裹,就意味着每次界面刷新时都会重新调用 splineBasedDecay
创建衰减配置的实例。而使用 rememberSplineBasedDecay
就可以优化该问题,且无需手动传入 density
参数。
看一下 rememberSplineBasedDecay
源码:
@Composable
actual fun <T> rememberSplineBasedDecay(): DecayAnimationSpec<T> {
val density = LocalDensity.current
return remember(density.density) {
SplineBasedFloatDecayAnimationSpec(density).generateDecayAnimationSpec()
}
}
首先也是通过 LocalDensity.current
获取屏幕像素密度,然后使用 remember
创建衰减配置实例,remember
参数传入了 density
,也就是当特殊情况屏幕密度发生变化时会重新创建衰减配置实例。
在开发中遇到要使用 splineBasedDecay
的时候一般直接使用 rememberSplineBasedDecay
即可。
思考:前面介绍 splineBasedDecay
是跟屏幕像素密度有关的,如果需求就是不想因为屏幕像素密度而导致不同设备表现不一样怎么办呢?或者动画作用的数值就是跟屏幕像素密度没关,比如作用于旋转角度的动画,此时怎么办呢?这个时候就不能使用 splineBasedDecay
,而是应该使用 exponentialDecay
。
exponentialDecay
exponentialDecay
是指数衰减,即动画速度按指数递减,他不依赖屏幕像素密度,可用于通用数据的衰减动画。其定义如下:
fun <T> exponentialDecay(
frictionMultiplier: Float = 1f,
absVelocityThreshold: Float = 0.1f
): DecayAnimationSpec<T>
有两个参数,且都有默认值,参数解析如下:
frictionMultiplier:摩擦系数,摩擦系数越大,速度减速越快,反之则减速越慢
absVelocityThreshold:绝对速度阈值,当速度绝对值低于此值时动画停止,这里的数值是指多少单位的速度,比如动画数值类型为 Dp,这里传 100f 即 100f * 1.dp
使用如下:
var move by remember { mutableStateOf(false) }
val animatable = remember { Animatable(30.dp, Dp.VectorConverter) }
val scope = rememberCoroutineScope()
Box(
Modifier
.padding(start = 30.dp, top = animatable.value)
.size(100.dp, 100.dp)
.background(Color.Blue)
.clickable {
scope.launch {
// 使用 exponentialDecay 衰减动画
animatable.animateDecay(1000.dp, exponentialDecay())
}
}
)
运行效果:
将摩擦系数设置为 5f 体验一下增加摩擦系数后的效果:
exponentialDecay(5f)
摩擦系数增大后,动画运行的距离和时间都明显缩短了。
将绝对速度阈值设置为 500f 再看一下效果:
exponentialDecay(absVelocityThreshold = 500f)
当动画速度达到阈值速度后动画就停止了,所以阈值越大动画越早停止。
实战
下面我们用衰减动画实现一个转盘抽奖的动画效果,即当点击抽奖后转盘开始转动然后缓缓停下,最后指针指向的位置就是中奖的奖品。
因为是旋转动画,所以这里我们使用 exponentialDecay
指数衰减动画,同时准备两张图片素材,如下:
将两张图片居中叠加,然后通过动画旋转下面的圆盘就完成了整个动画效果,代码如下:
// 创建动画实例
val animatable = remember { Animatable(0, Int.VectorConverter) }
// 获取协程作用域用户在按钮点击事件中开启协程
val scope = rememberCoroutineScope()
// 中奖结果
var luckyResult by remember { mutableStateOf("") }
// 中奖项
val luckyItem = remember { arrayOf("50元红包", "20元红包","10元红包","100-50券","小米蓝牙耳机","谢谢参与") }
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Box{
// 底部圆盘图片
Image(
painter = painterResource(R.drawable.bg),
contentDescription = "bg",
// 旋转角度设置为动画的值
modifier = Modifier.rotate(animatable.value.toFloat())
)
// 中间指针图片
Image(
painter = painterResource(R.drawable.center),
contentDescription = "center",
// 设置点击事件
modifier = Modifier.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
// 开启协程
scope.launch {
// 更新抽奖状态
luckyResult = "抽奖中"
// 开启动画
// 初始速度设置为 10000 再加上 1000~10000 的随机数
// 衰减曲线设置为 exponentialDecay 摩擦系数设置为 0.5f
val result = animatable.animateDecay(10000 + Random.nextInt(1000,10000), exponentialDecay(frictionMultiplier = 0.5f))
// 动画执行完后从动画结果中获取最后的值,即旋转角度
val angle = result.endState.value
// 通过计算获取当前指针在哪个范围
val index = angle % 360 / 60
// 获取中奖结果,并显示在屏幕上
luckyResult = luckyItem[index]
}
})
)
}
// 显示中奖结果
Text(luckyResult, modifier = Modifier.padding(10.dp))
// 添加重置按钮
Button(onClick = {
scope.launch {
// 通过 snapTo 瞬间回到初始状态
animatable.snapTo(0)
}
}){
Text("重置")
}
}
最终效果:
最后
本篇继 Animatable
的 animateTo
和 snapTo
后继续介绍了 animateDecay
衰减动画的使用,包括如何设置衰减曲线,不同衰减曲线的参数配置以及使用场景,并通过衰减动画实现了抽奖转盘效果。下一篇我们继续探索 Animatable
的边界设置及其相关的应用,请持续关注本专栏了解更多 Compose 动画内容。
来源:https://juejin.cn/post/7171420234811703333
猜你喜欢
- 前言这几天学习谷粒商城又再次的回顾了一次SpringCache,之前在学习谷粒学院的时候其实已经学习了一次了!!!这里就对自己学过来的内容进
- 委托这个东西不是很好理解,可是工作中又经常用到,你随处可以看到它的身影,真让人有一种又爱又恨的感觉,我相信许多人被它所困扰过。一提到委托,如
- 目录为什么要使用 TaskTask 和 Thread 区别Task 介绍Task 简单实现Task 执行状态1.等待(Wait)2. 返回值
- 采用继承Thead类实现多线程:优势:编写简单,如果需要访问当前线程,只需使用this即可,无需使用Thead.currentThread(
- Springboot对配置文件的敏感信息加密前言最近公司对软件的安全问题比较在意,要求对配置文件中的敏感信息如数据库密码等进行加密。但是Sp
- 最近做了一个功能,里面涉及到了渐变圆形的需求。就是一个颜色可以渐变的圆环,最后实现的效果如下图:左图是带渐变效果,右图是不带渐变效果。原理还
- 目录一、前言(1)Timer(2)DelayedQueue 延迟队列(3)ScheduledThreadPoolExecutor(4)Sch
- 服务端在平台上创建springboot小程序应用创建小程序登录蚂蚁金服开放平台,扫码登录填写信息后,点击支付宝小程序,选择立即接入 >
- 一、前言 Android 中解决滑动的方案有2种:外部拦截法 和内部拦截法。 滑动冲突也存在2种场景: 横竖滑动冲突、同向滑动冲突。 所以我
- 本文实例讲述了C#实现String类型和json之间的相互转换功能。分享给大家供大家参考,具体如下:////Donet2.0 需要添加引用/
- 这篇文章主要介绍了Java List分页功能实现代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的
- 在网上看到一些人写关于条形码的代码都很长,有的甚至拿来卖,所以查了下资料,希望能对大家有帮助。我的实现原理是:其实Windows本身就有一个
- 本文实例为大家分享了C#支付宝扫码支付示的具体代码,供大家参考,具体内容如下支付宝工具类using System; using System
- 一、流程图二、Token1、token是一种客户端认证机制,是一个经过加密的字符串,安全性强,支持跨域2、用户第一次登录,服务器通过数据库校
- 概述Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。Sentine
- 之前文章中我们讲到,java中实现同步的方式是使用synchronized block。在java 5中,Locks被引入了,来提供更加灵活
- Spring Boot怎么实现热部署在Spring Boot实现代码热部署是一件很简单的事情,代码的修改可以自动部署并重新热启动项目。1、引
- 当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模
- 大部分Java开发者都在使用Map,特别是HashMap。HashMap是一种简单但强大的方式去存储和获取数据。但有多少开发者知道HashM
- 一、背景在实际开发中,对于 不需要任何准确计算精度的属性可以直接使用float或double,但是如果需要精确计算结果,则必须使用BigDe