17.3 Compose 动画(三) —— animate*AsState 、Animatable 、Transition

1,097 阅读5分钟

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") }
          }
}

Untitled.gif

但是,我们看日志

Untitled.gif

改变 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))

EB06B42B-AC3A-4362-8ADA-0A797C79FF98.png

graphicsLayer 与 Modifier 干扰

graphicsLayer 已有属性的属性动画,同时使用 Modifier 与 graphicsLayer  设置动画会产生干扰。

.graphicsLayer {
	this.alpha = alpha
}
.offset { offset }

Untitled.gif

.graphicsLayer {
	this.alpha = alpha
	translationX = offsetX
}

Untitled.gif

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") }
    }
}

Untitled.gif

我们来对比一下上面 animate*AsState 中的动画效果

Untitled.gif

9FBEB01C-BFA3-4006-ACB5-12311EE10029.png

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}}"
}

E4DB3759-3923-4442-A238-5D5D9F2A82A1.png

第一次点击,当动画值到达 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 自动开启所有子动画。

使用方法

  1. val transition = updateTransition(targetState)  声明 Transition
  2. 使用 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") }
    }
}

Untitled.gif

D926A36F-2E97-4B55-BD45-1C82F0EDDB43.png

Animatable 只能设置一种类型的动画,Transition 每个子动画都可以是不同的类型。

Transition 更适合做复杂的动画,AnimatedVisibility、Crossfade 这些高级 Api 底层都是 Transition 来实现的