Jetpack Compose 动画初步了解和使用

Animatable


compose 使用 Animatable 来实现动画效果,Animatable 可以理解为一个可以作为动画属性的 Value 持有者。当它持有的 Value 通过 animateTo 更新时,可以自动以动画的形式对这一过程进行演变。与传统基于 View 实现的动画不同, 其内部使用协程计算动画的中间过程,所以触发函数 animateTo() 是用suspend 这大大保障了动画运行时的性能。基本的使用方式:


@Composable
fun Demo() {
var flag by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
.background(Color.DarkGray),
contentAlignment = Alignment.Center
) {
val animate = remember { Animatable(32.dp, Dp.VectorConverter) }
// 通过协程触发 animateTo()
LaunchedEffect(key1 = flag) {
animate.animateTo(if (flag) 32.dp else 144.dp)
}
Row(
Modifier
.size(animate.value) // size 在 animate 中取值
.background(Color.Magenta)
.clickable { flag = !flag }
) {}
}
}

首先看 Animatable :


androidx.compose.animation.core.Animatable

public constructor Animatable<T, V : AnimationVector>(
initialValue: T,
typeConverter: TwoWayConverter<T, V>,
visibilityThreshold: T?
)


  • initialValue 很好理解,作为它的初始值传入,所谓的 Value 持有者持有的就是它。
  • typeConverter 是用来统一动画行为,可以做属性动画的值都通过这个converter 把不同类型的值都转化成 Float 进行动画计算,与对应的 AnimationVector 进行互相转化。
  • visibilityThreshold 判断动画逐渐变为目标值得阈值,可空,暂且按下不表。

详细了解一下其中的 TwoWayConverter


/**
* [TwoWayConverter] class contains the definition on how to convert from an arbitrary type [T]
* to a [AnimationVector], and convert the [AnimationVector] back to the type [T]. This allows
* animations to run on any type of objects, e.g. position, rectangle, color, etc.
*/
interface TwoWayConverter<T, V : AnimationVector> {
/**
* Defines how a type [T] should be converted to a Vector type (i.e. [AnimationVector1D],
* [AnimationVector2D], [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of
* type T).
*/
val convertToVector: (T) -> V
/**
* Defines how to convert a Vector type (i.e. [AnimationVector1D], [AnimationVector2D],
* [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of type T) back to type
* [T].
*/
val convertFromVector: (V) -> T
}

TwoWayConverter 用于定义如何把任意类型的值与可供动画使用的 AnimationVector 之前互相转化的方法,这样通过对它的封装就可以进行对任意属性类型做统一的动画计算。同时,根据动画所需的维度数据返回对应维度的封装 AnimationVectorXD ,这里所说的XD 是指数据维度的个数。例如:



  • androidx.compose.ui.unit.Dp 值转化为 AnimationVector 只有一个维度,也就是它的 value ,所以转化为与之对应的 AnimationVector1D
  • androidx.compose.ui.geometry.Size 中包含两个维度的数据:widthheight , 所以对转化为 AnimationVector2D
  • androidx.compose.ui.geometry.Rect 中包含四个数据维度:lefttoprightbottom,对应 AnimationVector4D

同时,Compose 还对常用与动画的对象非常贴心的做了默认实现:



  • Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>
  • Int.Companion.VectorConverter: TwoWayConverter<Int, AnimationVector1D>
  • Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>
  • Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
  • DpOffset.Companion.VectorConverter: TwoWayConverter<DpOffset, AnimationVector2D>
  • Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>
  • Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>
  • IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>
  • IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>

至此,Animatable 有了初始值, 也有了值类型与对应动画数据的转换方式,那么只需要一个目标值,就满足触发动画的条件了。又因为动画数据的计算在协程中进行,那么我们此时只需在协程中触发 animateTo() 就可以了:


// 通过协程触发 animateTo()
LaunchedEffect(key = flag) {
animate.animateTo(if (flag) 32.dp else 144.dp)
}

注意此处的协程 CoroutineScope 是通过 Composable 函数 LaunchedEffect 提供的,该函数内部实现了对于 composer 的优化,同时通过 remember 函数缓存状态,所以不会由于 recompose 的主动或被动调用而多次执行。


AnimationSpec


AnimationSpec 顾名思义支持对动画定义规范,以此实现自定义动画。


查看 animateTo 函数的定义可以发现其第二个参数可以设置 animationSpec,它有一个默认的实现 defaultSpringSpec ,所以上面的例子中没有明确指定 animationSpec


androidx.compose.animation.core.Animatable 
public final suspend fun animateTo(
targetValue: T,
animationSpec: AnimationSpec<T> = defaultSpringSpec,
initialVelocity: T = velocity,
block: (Animatable<T, V>.() → Unit)? = null
): AnimationResult<T, V>

spring


defaultSpringSpec 是一个通过 spring 创建的基于弹簧的物理特性的动画:


androidx.compose.animation.core AnimationSpec.kt 
@Stable
public fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy,
stiffness: Float = Spring.StiffnessMedium,
visibilityThreshold: T? = null
): SpringSpec<T>

val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)

spring 接受两个参数,dampingRatiostiffness 。前者定义弹簧的弹性,默认值为 Spring.DampingRatioNoBouncy 。后者定义弹簧向 targetVaule 移动的速度。 默认值为 Spring.StiffnessMedium。基于物理特性的 spring 无法设置 duration。具体效果参考下图:


animation-spring.gif


tween


androidx.compose.animation.core AnimationSpec.kt 
@Stable
public fun <T> tween(
durationMillis: Int = DefaultDurationMillis,
delayMillis: Int = 0,
easing: Easing = FastOutSlowInEasing
): TweenSpec<T>

tween 在指定的 durationMillis 内使用缓和曲线在起始值和结束值之间添加动画效果。动画曲线通过 Easing 添加。


val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)

keyframes


keyframes 会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,可以指定 Easing 来确定插值曲线:


val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)

snapTo(targetValue: T)


androidx.compose.animation.core.Animatable
public final suspend fun snapTo(
targetValue: T
): Unit

Animatable 还提供了一个 snapTo(targetValue) 的函数,这个函数允许直接设置它内部持有的 value 值,此过程不会产生任何动画,正在进行的动画也会被取消,某些场景可能需要动画开始前有一个初始值,可以使用此函数。


一种更方便的使用方式:animate*AsState


设置某一个属性的目标值,当对应属性值发生变化后,自动触发动画,过度到对应值。



This Composable function is overloaded for different parameter types such as Float, Color, Offset, etc. When the provided targetValue is changed, the animation will run automatically. If there is already an animation in-flight when targetValue changes, the on-going animation will adjust course to animate towards the new target value.



compose 提供了这几个覆盖基本场景的函数:



  • animateFloatAsState
  • animateDpAsState
  • animateSizeAsState
  • animateOffsetAsState
  • animateRectAsState
  • animateIntAsState
  • animateIntOffsetAsState
  • animateIntSizeAsState

@Composable
fun Demo() {
var flag by remember { mutableStateOf(false) }
Box(
Modifier.fillMaxSize().background(Color.DarkGray),
contentAlignment = Alignment.Center
) {

val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
// 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
}
Row(
Modifier
.size(size)
.background(Color.Magenta)
.clickable { flag = !flag }
) {}
}
}

演示效果:


demo_animateDpAsState.gif


作为 compose 动画的最基本操作,与我们平时使用动画的方式不太一样,你会发现你能影响动画的核心只能是选一个属性和一个目标值。甚至连属性的初始值都不能预设,动画的时长没有办法干预。


深入一点点


查看 animateDpState() 函数的实现:


/**
* ... ...
*
* [animateDpAsState] returns a [State] object. The value of the state object will continuously be
* updated by the animation until the animation finishes.
*
* Note, [animateDpAsState] cannot be canceled/stopped without removing this composable function
* from the tree. See [Animatable] for cancelable animations.
*
* @sample androidx.compose.animation.core.samples.DpAnimationSample
*
* @param targetValue Target value of the animation
* @param animationSpec The animation that will be used to change the value through time. Physics animation will be used by default.
* @param finishedListener An optional end listener to get notified when the animation is finished.
* @return A [State] object, the value of which is updated by animation.
*/
@Composable
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
return animateValueAsState(
targetValue,
Dp.VectorConverter,
animationSpec,
finishedListener = finishedListener
)
}

查看 animateIntAsState 的实现:


/**
* ... ...
*
* [animateIntAsState] returns a [State] object. The value of the state object will continuously be
* updated by the animation until the animation finishes.
*
* Note, [animateIntAsState] cannot be canceled/stopped without removing this composable function
* from the tree. See [Animatable] for cancelable animations.
*
* @param targetValue Target value of the animation
* @param animationSpec The animation that will be used to change the value through time. Physics
* animation will be used by default.
* @param finishedListener An optional end listener to get notified when the animation is finished.
* @return A [State] object, the value of which is updated by animation.
*/
@Composable
fun animateIntAsState(
targetValue: Int,
animationSpec: AnimationSpec<Int> = intDefaultSpring,
finishedListener: ((Int) -> Unit)? = null
): State<Int> {
return animateValueAsState(
targetValue, Int.VectorConverter, animationSpec, finishedListener = finishedListener
)
}




  • targetValue是某一个以Dp为单位的属性的目标值,顾名思义就是你希望这个属性变化为某一个具体的值;




  • animationSpec 该属性的值如何跟随时间的变化而变化,有默认实现;




  • finishedListener 动画结束函数,可空;




  • anumate*AsState 系列函数的实现都很相似,统一在内部调用了 animateValueAsState(...)




这是一个基于 State 的实现,联系 Compose 中对于数据的封装和订阅方式,可以理解为当程序的某一个行为触发动画启动后,compose 会自主启动,并根据时间来计算对应的属性应该是什么值,再通过 State 返回,Composable 函数在一次次 recompose 行为中不断通过 State 获取到该属性的最新值,并刷新到界面上,知道这个值变化到目标值状态,更新也就结束了。也就是动画结束。


继续深入 animateValueAsState 的实现:


@Composable
fun <T, V : AnimationVector> animateValueAsState(
targetValue: T,
typeConverter: TwoWayConverter<T, V>,
animationSpec: AnimationSpec<T> = remember {
spring(visibilityThreshold = visibilityThreshold)
},
visibilityThreshold: T? = null,
finishedListener: ((T) -> Unit)? = null
): State<T> {

val animatable = remember { Animatable(targetValue, typeConverter) }
val listener by rememberUpdatedState(finishedListener)
val animSpec by rememberUpdatedState(animationSpec)

... ...

return animatable.asState()
}

你会发现其内部其实还是使用 Animatable 来实现。anumate*AsState 虽然基于 Animatable ,不但没有扩充 Animatable 的用法,反而还有了局限,怎会如此?个人认为 animate*AsState 是专门为确定性的简单使用场景进行的封装,这些场景有明确的状态变化,需要做动画的值也不会很复杂,在这些场景中如果能极为方便的快速定义动画,也会是一种非常实用的设计,即使场景变得复杂,再用 Animatable 兜底也能满足需求。


updateTransition


在实际的使用场景中,很多情况下的动画设计都不是单一参数可以完成的,比如大小变化的同时对颜色进行过渡、大小与圆角同时变化,形状与颜色同时变化等。这些情况需要组合多个动画同时进行:


@Composable
fun Demo() {
var flag by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
.background(Color.DarkGray),
contentAlignment = Alignment.Center
) {

val size by animateDpAsState(if (flag) 32.dp else 96.dp) { valueOnAnimateEnd ->
// 可以设置动画结束的监听函数,回调动画结束时对应属性的目标值
Log.i(TAG, "size animate finished with $valueOnAnimateEnd")
}
val color by animateColorAsState(
targetValue = if (flag) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
Row(
Modifier
.size(size)
.background(color)
.clickable { flag = !flag }
) {}
}
}

但是上面的实现存在一个问题,就是每一个属性值的动画过程都是单独计算的,同时每个属性动画也都要考单独的状态进行管理,这显然在性能上是有浪费的,而却也很不方便。这种情况可以引入 Transition 来进行动画的统一管理:


@Composable
fun Demo() {
var flag by remember { mutableStateOf(false) }
Box(
Modifier
.fillMaxSize()
.background(Color.DarkGray),
contentAlignment = Alignment.Center
) {
val transition = updateTransition(flag)
val size = transition.animateDp { if (it) 32.dp else 96.dp }
val color = transition.animateColor { if (it) MaterialTheme.colors.primary else MaterialTheme.colors.secondary }
Row(
Modifier
.size(size.value)
.background(color.value)
.clickable { flag = !flag }
) {}
}
}

这样当 flag 触发 transition 状态改变时,sizecolor 的值就可以同时在 transition 内部进行计算,性能又节省了亿点点🤏🏻


@Composable
fun <T> updateTransition(
targetState: T,
label: String? = null
): Transition<T> {
val transition = remember { Transition(targetState, label = label) }
transition.animateTo(targetState)
DisposableEffect(transition) {
onDispose {
// Clean up on the way out, to ensure the observers are not stuck in an in-between
// state.
transition.onTransitionEnd()
}
}
return transition
}

updateTransition 可以创建并保存状态,其内部使用 remember 实现。


作者:User_20211124110632
链接:https://juejin.cn/post/7034107134551785503
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册