17.2 Compose 动画(二) —— AnimationSpec , Easing

230 阅读5分钟

Animation 中 Spec 用来设置动画效果。Animation 接口有两个实现类  TargetBasedAnimation 和 DecayAnimation, 他们都有各自的 Spec 。

AnimationSpec

AnimationSpec 是TargetBasedAnimation 中使用的 Spec 。 TargetBasedAnimation 是属性值变化范围确定的动画,即有确定的初始值和结束值。下面的介绍中不再重复说明属性值这一条件。

蓝色的是接口,绿色的是类。

泛型 T (属性的数据类型) 的是调用者使用的 Spec

泛型 V 的是 <V:AnimationVector> 框架内部使用的,AnimationVector 我们上一篇有讲。

297FCC79-973C-4854-9D91-23B1D488402E.png

FloatAnimationSpec 接口专门处理 Float 类型数据 FloatTweenSpec 就相当于 TweenSpec<Float> 。 所以我们只需要了解六个实现类就可以了,我们再把这六个 Spec 分个类。

63F7EBBA-30C6-40CA-9174-29BFC7325B54.png 单次中根据参数不同我们把这四个 Spec 分成时间和弹性两种,先来看弹性

SpringSpec

    <T : Any?> SpringSpec( 
	dampingRatio: Float,// 阻尼比
	stiffness: Float, //刚性
    visibilityThreshold: T?//可见阈值
)

弹性动画

Untitled.gif

Untitled.gif

Untitled.gif

466A5027-B07E-4851-A19B-58C6D52BAD75.png

对比上图

  • dampingRatio: 阻尼比越小弹的幅度越大,且弹的次数越多
  • stiffness: 刚性越大弹的过程中回到 target 值的时间越短
  • visibilityThreshold: T?: 动画可见阈值 动画值小于阈值后动画值不再变化一直是 targetValue

可用常量值

object Spring {
    const val StiffnessHigh = 10_000f
    const val StiffnessMedium = 1500f
    const val StiffnessMediumLow = 400f
    const val StiffnessLow = 200f
    const val StiffnessVeryLow = 50f

    const val DampingRatioHighBouncy = 0.2
    const val DampingRatioMediumBouncy = 0.5f
    const val DampingRatioLowBouncy = 0.75f
    const val DampingRatioNoBouncy = 1f
    
    const val DefaultDisplacementThreshold = 0.01f
}

TweenSpec

TweenSpec(
  durationMillis: Int,//动画的时长
  delay: Int,//动画延
  easing: Easing //插值器
)

可以设置动画时长、延时、插值器。Easing 就是插值器,换成 Interpolator 大家就熟悉了,用来设置动画效果。Easing 我们最后详细介绍。

Untitled.gif

Untitled.gif

Untitled.gif

KeyframesSpec

<T : Any?> KeyframesSpec(config: KeyframesSpec.KeyframesSpecConfig<T>) 

也是基于时长的 Spec ,不同于 tween 它可以通过 config 指定关键帧( 值 at time)。

相当于用关键帧将 Spec 分成了几段 TweenSpec,而且分出来的每段都可以指定一个 Easing 。

15EEE364-E22F-4292-9BE9-64BF94A4EB77.png

SnapSpec

<T : Any?> SnapSpec(delay: Int)

没有动画效果,作用就是设置一个延时,经过 dealay 毫秒后直接变成 targetValue。

Untitled.gif

RepeatableSpec

<T : Any?> RepeatableSpec(
    iterations: Int, // 重复几次
    animation: DurationBasedAnimationSpec<T>,
    repeatMode: RepeatMode, 
    initialStartOffset: StartOffset
)

Untitled.gif

Untitled.gif

InfiniteRepeatableSpec

<T : Any?> InfiniteRepeatableSpec(
    animation: DurationBasedAnimationSpec<T>,
    repeatMode: RepeatMode,
    initialStartOffset: StartOffset
)

比 RepeatableSpec 少了  iterations 参数,因为它会一直重复 。

Easing

除了 SpringSpec 和 SnapSpec  动画效果都是由 Easing 控制的。还记得上一篇文章我们的 MyTargetBasedAnimation 值的计算么?

    override fun getValueFromNanos(playTimeNanos: Long): T {
        val valueDiff = targetValueVector.value - initValueVector.value
        val progress = playTimeNanos / durationNanos.toFloat()
        val valueVector = AnimationVector1D(initValueVector.value + valueDiff * progress)
        return typeConverter.convertFromVector(valueVector)
    }

progress 是根据时间计算出动画的进度再乘以结束值和初始值的差来计算出动画进行的差量。时间是线性增加的所以整个动画的效果也是线性的。

Easing 接口中的 ,transfrom 方法传入的 fraction 就是上面的 progress ,返回一个新的 progress 。算法不同返回的 progress 不同,计算出的动画差量也不同 ,这样就可以产生不同的动画效果了。

@Stable
fun interface Easing {
    fun transform(fraction: Float): Float
}

Compose 中提供了 4 中常用的 Easing 。 LinearEasing 就是传入什么返回什么,间接验证了我们上面的说法。

val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)

val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)

val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)

val LinearEasing: Easing = Easing { fraction -> fraction }

CubicBezierEasing

除了 LinearEasing 其他的都是通过 CubicBezierEasing 实现的,所以我们也可以通过 CubicBezierEasing 来自定义 Easing 。

class CubicBezierEasing(
    private val a: Float,
    private val b: Float,
    private val c: Float,
    private val d: Float
) : Easing 

通过三个点可以画一个贝塞尔曲线

1572CDF0-F0F4-4D69-83F3-5FD3A2A63F27.gif

CubicBezierEasing 中一共有 5 个点

  • Ps (0,0) 初始点,
  • P1(a,b) 第一个控制点,
  • Pc(0.5,0.5) 中心点
  • P2(c,d) 第二个控制点
  • Pe(1,1) 结束点

分成两组 Ps ,P1,Pc ; Pc,P2,Pe , x 坐标是时间

0CA16988-A9AA-4683-ABF4-ED9A9D2AA104.png

Interpolator 转换成 Easing

添加拓展函数

fun TimeInterpolator.toEasing() = Easing { x -> getInterpolation(x) }

这样就可以使用 android.animation 包中实现 TimeInterpolator 接口的插值器了。

Untitled.gif

本文代码

代码比较简单直接贴了

AnimationTrackPanel.kt

private const val TAG = "AnimationTrackPanel"
@Composable
fun AnimationTrackPanel(minY: Int, maxY: Int,durationInMs:Int,spec: AnimationSpec<Float>,specDesc:String) {
    val textSize = 16.sp
    Column(
        Modifier
            .fillMaxSize()
            .padding(top = 10.dp, end = 10.dp)) {
        Row(modifier = Modifier.weight(1f)) {
            Box(modifier = Modifier
                .fillMaxHeight()
                .width(20.dp)){
                YAxleLabel(minY,maxY,textSize)
            }
            Box(modifier = Modifier.fillMaxSize()){
                Canvas(modifier = Modifier.fillMaxSize()){
                    val paintColor = Color.Black
                    drawRect(paintColor, size = size, style = Stroke(1.dp.toPx()))
                    (minY +1 until  maxY).forEach { value ->
                        val offsetY = size.height - size.height * ((value - minY).toFloat() / (maxY - minY))
                        drawLine(paintColor, Offset(0f,offsetY), Offset(size.width,offsetY))
                    }
                }
                AnimationTrackPath(minY,maxY,durationInMs,spec)

            }
        }
        // X Label
        Row(modifier = Modifier.fillMaxWidth().padding(start = 20.dp), horizontalArrangement = Arrangement.SpaceBetween) {
            Text(text = "[  $specDesc ]", fontSize = textSize)
            Text(text = "${durationInMs}ms", fontSize = textSize)
        }
    }
}

@Composable
fun AnimationTrackPath(minY: Int, maxY: Int,durationInMs:Int,spec: AnimationSpec<Float>){
    // forXAxle
    val timeProgress = remember(spec) { Animatable(0f) }
    //forYAxle
    val specTrack = remember(spec) { Animatable(0f) }

    LaunchedEffect(timeProgress){
        timeProgress.animateTo(1f, tween(durationInMs, easing = LinearEasing))
    }
    LaunchedEffect(specTrack){
        specTrack.animateTo(1f, animationSpec = spec)
    }

    val path by remember(spec) { mutableStateOf(Path()) }

    Canvas(modifier = Modifier.fillMaxSize()){

        val y =  size.height * (1 - (specTrack.value - minY) / (maxY - minY))
        if (path.isEmpty){
            path.moveTo(0f,y)
        }
        val x =  timeProgress.value * size.width
        path.lineTo(x,y)
        drawPath(path, Color.Red,  style = Stroke(2.dp.toPx()))
    }
}

@Composable
fun YAxleLabel(minValue: Int, maxValue: Int, textSize: TextUnit){
    Canvas(modifier = Modifier.fillMaxHeight()) {
        val textPaint = Paint().asFrameworkPaint().apply {
            this.textSize = textSize.toPx()
        }
        drawIntoCanvas { canvas ->
            for(value in minValue .. maxValue){
                val offsetY = size.height - size.height * ((value - minValue).toFloat() / (maxValue - minValue))
                val textY = offsetY + (textSize.toPx() / 2)
                canvas.nativeCanvas.drawText(
                    value.toString(),
                    0f,
                    textY,
                    textPaint
                )
            }
        }
    }
}    

SpecsEnum.kt

enum class SpecsEnum(
    val minValue: Int,
    val maxValue: Int,
    val durationMs: Int,
    val spec: AnimationSpec<Float>,
    val desc: String
) {
    Spring_02_200(
        0, 2, 5000,
        spring(dampingRatio = 0.2f, stiffness = 200f),
        "spring(dampingRatio = 0.2f, stiffness = 200f)"
    ),
    Spring_04_200(
        0, 2, 5000,
        spring(dampingRatio = 0.4f, stiffness =200f),
        "spring(dampingRatio = 0.4f, stiffness =200f)"
    ),
    Spring_02_400(
        0, 2, 5000,
        spring(dampingRatio = 0.2f, stiffness = 400f),
        "spring(dampingRatio = 0.2f, stiffness = 400f)"
    ),
    Spring_02_400_02(
        0, 2, 5000,
        spring(dampingRatio = 0.2f, stiffness = 400f, visibilityThreshold = 0.2f),
        "spring(dampingRatio = 0.2f, stiffness = 400f, visibilityThreshold = 0.2f)"
    ),
    Tween_LinearEasing(
        0, 1, 1000,
        tween(durationMillis = 1000, easing = LinearEasing),
        "tween(durationMillis = 1000, easing = LinearEasing)"
    ),
    Tween_LinearDelay(
        0, 1, 2000,
        tween(durationMillis = 1000, delayMillis = 500, easing = LinearEasing),
        "tween(durationMillis = 1000, delayMillis = 500, easing = LinearEasing)"
    ),
    Tween_FastOutSlowIn(
        0, 1, 1000,
        tween(durationMillis = 1000, easing = FastOutSlowInEasing),
        "tween(durationMillis = 1000, easing = FastOutSlowInEasing)"
    ),
    KeyFrames(
        0, 1, 2000,
        keyframes {
            durationMillis = 2000
            0f at 0  with FastOutSlowInEasing // 可省略
            0.25f at 1000
            0.5f at 1600  with  LinearOutSlowInEasing
            1f at 2000 //可省略
        },
        "keyframes"
    ),
    Snap_500(
        0, 1, 1000,
        snap(delayMillis = 500),
        "snap(delayMillis = 500)"
    ),
    RepeatableReverse(
        0, 1, 2000,
        repeatable(
            iterations = 2,
            animation =  tween(durationMillis = 1000, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        "repeatable(2,tween(1000,LinearEasing),RepeatMode.Reverse)"
    ),
    RepeatableRestart(
        0, 1, 2000,
        repeatable(
            iterations = 2,
            animation =  tween(durationMillis = 1000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        "repeatable(2,tween(1000,LinearEasing),RepeatMode.Restart)"
    ),
    CubicBezierEasing(
        0, 1, 1000,
        tween(durationMillis = 1000, easing = CubicBezierEasing(0.25f,1f,0.75f,0f)),
        "CubicBezierEasing(0.25f,1f,0.75f,0f)"
    ),
    BounceInterpolator(
        0, 1, 2000,
        tween(durationMillis = 1000, easing = BounceInterpolator().toEasing()),
        "BounceInterpolator"
    )

}

fun TimeInterpolator.toEasing() = Easing {
        x -> getInterpolation(x)
}    

MainActivity.kt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeAnimationSpecTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    var currentSpec by remember { mutableStateOf(SpecsEnum.Spring_02_200) }
    Row(modifier = Modifier.fillMaxSize()) {
        LazyColumn(modifier = Modifier.padding(horizontal = 10.dp)) {
            items(SpecsEnum.values()) {
                Button( onClick = { currentSpec = it }, shape = RoundedCornerShape(2.dp)) {
                    Text(text = it.name) }
            }
        }

        AnimationTrackPanel(
            minY = currentSpec.minValue,
            maxY = currentSpec.maxValue,
            durationInMs = currentSpec.durationMs,
            spec = currentSpec.spec,
            specDesc = currentSpec.desc
        )
    }
}