animate*AsState
最简单的 Compose 动画 API,用于单个属性值动画,Compose 针对不同属性类型提供了如下 API。
animateFloatAsState, animateDpAsState, animateSizeAsState, animateOffsetAsState, animateRectAsState, animateIntAsState,
animateIntOffsetAsState, animateIntSizeAsState
这些 API 内部都是使用 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>
使用起来很方便,指定不同状态下的目标值即可, AnimationSpec 默认是 spring
@Composable
fun AnimateFloatAsState() {
var switch by remember { mutableStateOf(false) }
val alpha by animateFloatAsState(
targetValue = if (switch) 1f else 0.3f,
animationSpec = tween(500)
)
Log.e(TAG, "AnimateFloatAsState ")
Column {
Box(modifier = Modifier
.size(100.dp)
.graphicsLayer {
this.alpha = alpha
}
.background(Color.Red))
Button(onClick = {
Log.e(TAG, "Switch Alpha Click")
switch = !switch
}) { Text(text = "Switch Alpha") }
}
}
enum class OffsetState(val value:Int){
Zero(0),Mid(100),End(200)
}
@Composable
fun AnimateOffsetAsState() {
var offsetState by remember { mutableStateOf(OffsetState.Zero) }
val offset by
animateIntOffsetAsState(
targetValue = IntOffset(offsetState.value, 0) ,
animationSpec = spring(Spring.DampingRatioHighBouncy)
)
Log.e(TAG, "AnimateOffsetAsState ")
Column {
Box(modifier = Modifier
.size(100.dp)
.offset { offset }
.background(Color.Cyan))
Button(onClick = {
offsetState = when(offsetState){
OffsetState.Zero -> OffsetState.Mid
OffsetState.Mid -> OffsetState.End
OffsetState.End -> OffsetState.Zero
}
Log.e(TAG, "Switch Offset Click")
}) { Text(text = "Switch Offset") }
}
}
但是,我们看日志
改变 alpha 的动画过程中重组了 N 次,改变 Offset 的动画并没有。
这是 Compose 中很常见的 State 值改变触发重组的情况。使用 State.value 的函数会被记录成观察点,一旦 State.value 值改变就会触发该函数的重组。
Offset 动画中对于 OffsetAsState 的使用是这样的
//offset.value 是在 offset() 提供的 lambda 中 使用
Box(modifier = Modifier.size(100.dp).offset { offset }.background(Color.Cyan))
反观 alpha 动画直接在 Modifier 的 alpha() 中使用的 alphaState.value 。这样 AnimateFloatAsState() 函数就会被记录成观察点。
State 使用过程中要注意 State.value 调用位置,提供 lambda 入参时尽量在 lambda 中调用 State.value。
又但是,Modifier.alpha() 并没有提供 lambda 入参 O(∩_∩)O
Modifier.graphicsLayer
@Stable
fun Modifier.graphicsLayer(
scaleX: Float = 1f,
scaleY: Float = 1f,
alpha: Float = 1f,
translationX: Float = 0f,
translationY: Float = 0f,
shadowElevation: Float = 0f,
rotationX: Float = 0f,
rotationY: Float = 0f,
rotationZ: Float = 0f,
cameraDistance: Float = DefaultCameraDistance,
transformOrigin: TransformOrigin = TransformOrigin.Center,
shape: Shape = RectangleShape,
clip: Boolean = false,
renderEffect: RenderEffect? = null
)
alpha、scale 、translation 、rotation 、shape、clip 就是这个熟悉味道 。这些都可以在 Modifier 中找到对应的方法 ,Modifier 只有 Modifier.Offset 提供了 lambda 调用方式,graphicsLayer 提供了 Modifier.graphicsLayer{} 的调用方式,所有参数都可以在 lambda 中使用,而且graphicsLayer 参数更加详细 。
Box(modifier = Modifier.size(100.dp).graphicsLayer{
this.alpha = alpha
}.background(Color.Red))
graphicsLayer 与 Modifier 干扰
graphicsLayer 已有属性的属性动画,同时使用 Modifier 与 graphicsLayer 设置动画会产生干扰。
.graphicsLayer {
this.alpha = alpha
}
.offset { offset }
.graphicsLayer {
this.alpha = alpha
translationX = offsetX
}
Animatable
class Animatable<T, V : AnimationVector>(
initialValue: T,
val typeConverter: TwoWayConverter<T, V>,
private val visibilityThreshold: T? = null
)
Animatable 本身持有动画值(Animatable.value),创建时需要提供动画的初始值,通过 animateTo(targetValue)、animateDecay(initialVelocity)、snapTo(targetValue) suspend 方法来改变动画值。
typeConverter 用来做泛型转换这个我们在动画第一章的时候已经讲过了。Compose 默认提供了 Color 和 Float 类型的 Animatable 对象创建方法,其他类型需要我们手动传入 typeConverter
动画需要我们自己在协程中启动,一种是 LaunchedEffect
@Composable
fun AnimatableDemo(){
var offsetState by remember { mutableStateOf(OffsetState.Zero) }
val anim = remember { Animatable(
initialValue = OffsetState.Zero.value,
typeConverter = Int.VectorConverter
)}
//启动动画
LaunchedEffect(offsetState){
when(offsetState){
OffsetState.Mid -> anim.animateTo(
targetValue = offsetState.value,
animationSpec = tween(1000)
)
OffsetState.End -> anim.animateTo(offsetState.value, spring(Spring.DampingRatioHighBouncy))
OffsetState.Zero -> anim.snapTo(offsetState.value)
}
}
Column {
Box(modifier = Modifier
.size(100.dp)
.offset { IntOffset(anim.value, 0) }
.background(Color.Cyan))
Button(onClick = {
offsetState = when(offsetState){
OffsetState.Zero -> OffsetState.Mid
OffsetState.Mid -> OffsetState.End
OffsetState.End -> OffsetState.Zero
}
Log.e(TAG, "Switch Offset Click")
}) { Text(text = "Switch Offset") }
}
}
或者是 scope.launch {}
@Composable
fun AnimatableDemo(){
var offsetState by remember { mutableStateOf(OffsetState.Zero) }
val anim = remember { Animatable(
initialValue = OffsetState.Zero.value,
typeConverter = Int.VectorConverter
)}
val scope = rememberCoroutineScope()
Column {
Box(modifier = Modifier
.size(100.dp)
.offset { IntOffset(anim.value, 0) }
.background(Color.Cyan))
Button(onClick = {
offsetState = when(offsetState){
OffsetState.Zero -> {
scope.launch { anim.animateTo(OffsetState.Mid.value,tween(1000)) }
OffsetState.Mid
}
OffsetState.Mid -> {
scope.launch { anim.animateTo(OffsetState.End.value,spring(Spring.DampingRatioHighBouncy)) }
OffsetState.End
}
OffsetState.End -> {
scope.launch { anim.snapTo(OffsetState.Zero.value) }
OffsetState.Zero
}
}
Log.e(TAG, "Switch Offset Click")
}) { Text(text = "Switch Offset") }
}
}
我们来对比一下上面 animate*AsState 中的动画效果
Animatable 每次只能在协程中启动动画,而且每次都要指定 AnimationSpec。Animatable 作为 animateAsState 的底层实现,使用起来比 animateAsState 灵活,但复杂。而且 Animatable 还可以使用 animateDecay(initialVelocity)、snapTo(targetValue) 启动动画。
updateBounds 和 AnimationResult
fun updateBounds(lowerBound: T?, upperBound: T?)
设置动画值变化的范围,当动画值超过设置的范围后就算没有到达 targetValue 也会立即停止动画
class AnimationResult<T, V : AnimationVector>(
//结束时的状态
val endState: AnimationState<T, V>,
//结束原因 : Finished 正常结束 , BoundReached 到达设置的边界
val endReason: AnimationEndReason//
)
Animatable 的 animateTo(targetValue)、animateDecay(initialVelocity) 都会返回 AnimationResult。 下面的 Demo 设置的 targetValue 是 150 ,设置的上界是 100 ,我们来观察日志。
@Composable
fun AnimatableUpdateBoundsDemo(){
val scope = rememberCoroutineScope()
val anim = remember { Animatable(0,Int.VectorConverter)}
anim.updateBounds(lowerBound = 0, upperBound = 100)
Column {
Box(modifier = Modifier.size(100.dp).offset { IntOffset(anim.value, 0) }.background(Color.Cyan))
Button(onClick = {
scope.launch {
val target = if (anim.value == 0) 150 else 0
val result = anim.animateTo(target)
Log.e(TAG, "AnimatableDemo: ${result.endReason} ${result.endState.log()}")
}
}) { Text(text = "Click") }
}
}
fun <T, V : AnimationVector> AnimationState<T,V>.log():String {
return "AnimationState:{value:${this.value},velocity:${this.velocity}}"
}
第一次点击,当动画值到达 100 上界时,虽然此时的速度还是 1152 但是动画停止了,原因是 BoundReached 。 第二次点击,动画值到达 targetValue 时,速度也为0 是正常停止。
Transition
Modifier.graphicsLayer 例子完整的代码是这样的
@Composable
fun AnimateFloatAsState() {
var switch by remember { mutableStateOf(false) }
val alpha by animateFloatAsState(
targetValue = if (switch) 1f else 0.3f,
animationSpec = tween(1000)
)
val offsetX by
animateFloatAsState(
targetValue = if (switch) 100f else 0f,
animationSpec = spring(Spring.DampingRatioHighBouncy)
)
Column {
Box(modifier = Modifier.size(100.dp)
.graphicsLayer {
this.alpha = alpha
this.translationX = offsetX
}
.background(Color.Red))
Button(onClick = {
switch = !switch
Log.e(TAG, "Switch Click")
}) { Text(text = "Switch") }
}
}
如果我想加个旋转就要再声明一个 rotateState
val rotateZ by animateFloatAsState(targetValue = if (switch) 0f else 360f)
这些动画属性他们都是由 switch 这个一个状态控制的,这种情况我们使用 Transition
Transition 管理状态,子动画(1~N个)根据 Transition 中每个状态变化定义自己的动画,状态变化时 Transition 自动开启所有子动画。
使用方法
- val transition = updateTransition(targetState) 声明 Transition
- 使用 transition.animate*() 定义子动画
Transition 默认提供了下列创建子动画的方法
animateFloat, animateDp, animateOffset, animateSize, animateIntOffset, animateInt, aniateIntSize, animateRect
其他类型可以使用 animateValue 传入 TwoWayConverter 这个套路我们已经很熟了,就不讲了
@Composable
fun TransitionDemo() {
var switch by remember { mutableStateOf(false) }
val transition = updateTransition(targetState = switch,"Graphics")
val alpha by transition.animateFloat(
transitionSpec = { tween(AnimationConstants.DefaultDurationMillis) },
label = "alpha"
) { state: Boolean ->
if (state) 1f else 0.3f
}
val translationX by transition.animateFloat(label = "offsetX") {
if (it) 100f else 0f
}
val rotationZ by transition.animateFloat(label = "rotateZ") {
if (it) 0f else 360f
}
val bgColor by transition.animateColor { if (it) Color.Green else Color.Red}
Log.e(TAG, "TransitionDemo: ", )
Column {
Box(modifier = Modifier
.size(100.dp)
.graphicsLayer {
this.alpha = alpha
this.translationX = translationX
this.rotationZ = rotationZ
shape = RoundedCornerShape(10.dp)
clip = switch
}
.drawWithCache {
onDrawBehind {
drawRect(color = bgColor)
}
}
)
Button(onClick = {
switch = !switch
Log.e(TAG, "Switch Click")
}) { Text(text = "Switch") }
}
}
Animatable 只能设置一种类型的动画,Transition 每个子动画都可以是不同的类型。
Transition 更适合做复杂的动画,AnimatedVisibility、Crossfade 这些高级 Api 底层都是 Transition 来实现的