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
中包含两个维度的数据:width
和height
, 所以对转化为AnimationVector2D
androidx.compose.ui.geometry.Rect
中包含四个数据维度:left
,top
,right
,bottom
,对应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
接受两个参数,dampingRatio
和 stiffness
。前者定义弹簧的弹性,默认值为 Spring.DampingRatioNoBouncy
。后者定义弹簧向 targetVaule
移动的速度。 默认值为 Spring.StiffnessMedium
。基于物理特性的 spring
无法设置 duration
。具体效果参考下图:
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 }
) {}
}
}
演示效果:
作为 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
状态改变时,size
和 color
的值就可以同时在 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。