17.5 Compose 动画(五)——使用 lerp 函数拓展 TextStyle 动画

678 阅读1分钟

lerp函数

线性插值函数有三个参数,start、end 、 fraction ( 取值范围[0,1] ) ,返回 [start,end] 区间内 fraction 对应的值。

例如 lerp(0, 100, 0.5)  => 50  。

这跟动画有什么关系呢?

275C1095-6241-44B1-BD27-C6658F9254B4.png

看最后一行,名字都一样 ,这不就是 BaseTargetAnimation 每次屏幕刷新时取动画值嘛。

再看下 Compose 中提供了下面这些类型的 lerp 函数

B7CBAB85-6223-4C9B-948B-275F00E37824.png

Dp、Color 这些已经提供 TwoWayConverter 的类型可以直接使用动画 API ,就没必要再用 lerp 函数拓展了。

没有实现 TwoWayConverter 的类型就可以使用 lerp 函数来拓展动画功能了,特别是像 TextStyle 这种好多类型组合在一起的类型。

拓展的思路也很简单,就是对 fraction 参数做一个属性动画,函数返回值也会随着 fraction 变化而变化。

lerp 实现 TextStyle 动画

@Composable
fun LerpDemo(){
    val scope = rememberCoroutineScope()
    val textStyleStart = MaterialTheme.typography.bodyLarge //start
    val textStyleStop = MaterialTheme.typography.titleLarge  // stop
    val fractionAnim = remember { Animatable(0f) } // fraction
    
    val animTextStyle = lerp(textStyleStart,textStyleStop,fractionAnim.value) 

    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Animation", style = animTextStyle)
        Button(onClick = {
            val targetValue = if (fractionAnim.value > 0) 0f else 1f
            scope.launch {
                fractionAnim.animateTo(targetValue, spring(0.4f))
            }
        }) {
            Text(text = "Switch")
        }
    }
}

Untitled.gif

animateTextStyleAsState

上面用 lerp 实现了 TextStyle 动画,那么我们模仿 animateValueAsState 封装一个 animateTextStyleAsState ,以后就可以像 animate*AsState 一样调用了,岂不美滋滋。

@Composable
fun TextStyleDemo(){
    var switch by remember { mutableStateOf(false) }

    val textStyleStart = MaterialTheme.typography.bodyLarge //start
    val textStyleStop = MaterialTheme.typography.titleLarge  // stop
    val targetValue = if (switch) textStyleStop else textStyleStart

    val animTextStyle by animateTextStyleAsState(targetValue = targetValue,
        animationSpec = tween(500, easing = BounceInterpolator().toEasing())
    )

    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "Animation", style = animTextStyle)
        Button(onClick = { switch = !switch}) {
            Text(text = "Switch")
        }
    }
}

@Composable
fun animateTextStyleAsState(
    targetValue: TextStyle,
    animationSpec: AnimationSpec<Float> = remember { spring() },
    finishedListener: ((TextStyle) -> Unit)? = null
): State<TextStyle> {

    val listener by rememberUpdatedState(finishedListener)
    val animSpec by rememberUpdatedState(animationSpec)

    var startTextStyle by remember { mutableStateOf(targetValue) }
    val fractionAnim = remember { Animatable(0f) }

    val textStyleState = remember(fractionAnim.value){
        derivedStateOf {
            if (fractionAnim.value == 0f){
                startTextStyle
            }else{
                lerp(startTextStyle,targetValue,fractionAnim.value)
            }
        }
    }

    LaunchedEffect(targetValue) {
        if (targetValue != textStyleState.value){
            startTextStyle = textStyleState.value
            fractionAnim.snapTo(0f)Untitled.gif
            fractionAnim.animateTo(1f, animSpec)
            listener?.invoke(textStyleState.value)
        }
    }
    return textStyleState
}

Untitled.gif

注意 SpringSpec 应用在 Color 类型上可能会引起值越界。