Compose Compose动画之AnimateSpec

  • animateXXXAsState的使用
  • Animatable
  • 几种常用的AnimationSpec

animateXXXAsState的使用

举个栗子:实现一个红色方块,对它进行动画式的放大和缩放

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  var big by mutableStateOf(false)
  setContent {
    // big 为 true,目标值是 96.dp;否则为 48.dp
    val size by animateDpAsState(if (big) 96.dp else 48.dp)
    Box(
    Modifier
        .size(size) 
        .background(Color.Red)
        .clickable {
            big = !big
        })
  }
}

使用Compose为我们提供的animateXXXAsState函数,就可以非常方便的设置,比如这里目标值的单位是dp,所以就可以使用

animateDpAsState函数。除此之外类似的函数,还有animateRectAsStateanimateIntAsState等等。

注意:size不能定义成var类型:

animateDpAsState是个State而不是MutableState,所以它是只读不能写的

写起来确实非常简单,我们都知道这是提供的APi内部封装好的缘故,封装好的另一面必然是抛弃了一定的灵活性。正是如此,如果想要对动画过程进行精细化的定制,比如想把方块从48dp瞬间移到200dp,再慢慢缩放到96dp,animateXXXAsState就不能满足我们的需求了。

Animatable

对于方块从48dp瞬间移到200dp,再慢慢缩放到96d这个需求,我们需要用到Animatable类,它是一个偏底层的动画类,上述提到的animateDpAsState的内部实现就是借助了Animatable

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  var big by mutableStateOf(false)
  setContent {
    val anim = remember{ Animatable(48.dp, Dp.VectorConverter) }  //1
    // 这里要使用Compose提供的协程
    LaunchedEffect(big){        //2
      // 瞬间移动目标值
      anim.snapTo(200.dp)
      // 动画的方式慢慢到目标值
      anim.animateTo(96.dp)
    }
    Box(
        Modifier
            .size(anim.value)  //3
            .background(Color.Red)
            .clickable {
                big = !big
    }              
   }
}

这段代码,需要注意三个地方:

1.Animatable(xxx)

使用Animatable类的构造函数,可以定义动画的初始值,这里和animateDpAsState不同的是,参数不能直接使用dp,默认是float类型,所以需要后面添加一个Dp.VectorConverter来帮我们作dp->float的转换。其他类型的转换,官方也为我们提供了很多函数可直接调用。

2.LaunchedEffect(xxx)

fun LaunchedEffect(
    key1: Any?,
    block: suspend CoroutineScope.() -> Unit
) {
    val applyContext = currentComposer.applyCoroutineContext
    remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

LaunchedEffect函数的两个参数,key表示任意类型的值,当key的值发生变化,内部的代码块block就会再次执行。

观察这个block的类型,发现它用到了协程,这就意味着执行的动画需要是个挂起函数:

suspend fun snapTo(targetValue: T) {
    mutatorMutex.mutate {
        endAnimation()
        val clampedValue = clampToBounds(targetValue)
        internalState.value = clampedValue
        this.targetValue = clampedValue
    }
}

suspend fun animateTo(
    targetValue: T,
    animationSpec: AnimationSpec<T> = defaultSpringSpec,
    initialVelocity: T = velocity,
    block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V> {
    val anim = TargetBasedAnimation(
        animationSpec = animationSpec,
        initialValue = value,
        targetValue = targetValue,
        typeConverter = typeConverter,
        initialVelocity = initialVelocity
    )
    return runAnimation(anim, initialVelocity, block)
}

那可以用自己launch的协程吗?比如这样:

lifecycleScope.launch {
    // 瞬间移动目标值
    anim.snapTo(200.dp)
    // 动画的方式慢慢到目标值
    anim.animateTo(96.dp)
}

敲上这段代码,编译器就会提示要用LaunchedEffect

Calls to launch should happen inside a LaunchedEffect and not composition

这是因为,自己launch的协程,它是不支持重组优化的,这就意味着重组过程会重复执行该动画。第一浪费资源,第二很可能不是我们期待的视觉效果。

LaunchedEffect,却可以根据某个值是否变化了,再次执行动画,这个过程变得可控了。如我们例子中的big,在点击box控件后就会被改变,所以动画就会被再次执行。

3.anim.value

通过Animatablevalue属性可以获取其具体值,设置到控件。

几种常用的AnimationSpec

TweenSpec

上述的动画变换的控制,还是过于简单。如果想做些更精细的控制,比如控制动画过程的时长,动画的出入场的速度。此时就要了解一下动画函数的其他参数。

回顾一下上面用到的animateTo函数,之前只用到了targetValue,它的第二个参数animationSpec就可以帮助我们更精细的控制动画过程,它是AnimationSpec的对象,并且提供了一个默认值defaultSpringSpec,这个默认值我们后面在介绍。

suspend fun animateTo(
    targetValue: T,
    animationSpec: AnimationSpec<T> = defaultSpringSpec,
    initialVelocity: T = velocity,
    block: (Animatable<T, V>.() -> Unit)? = null
)

internal val defaultSpringSpec: SpringSpec<T> =
    SpringSpec(visibilityThreshold = visibilityThreshold)

比如我们对一个方块做一个匀速动画,可以用TweenSpec来设置动画函数的第二个参数animationSpec

TweenSpec(easing = LinearEasing, durationMillis = 2000)

其中durationMillis表示动画时长2000ms, easing就表示动画缓动的方式,和属性动画中的Interpolator作用是一样的。LinearEasing表示匀速,官方提供了四种常用的缓动方式:

  • LinearEasing:匀速

  • FastOutSlowInEasing: 先加速后减速

  • FastOutLinearInEasing:加速出场

  • LinearOutSlowInEasing:减速入场

比如下面的代码就可以实现,2s内方块匀速放大缩小的动画效果:

Column {
    LaunchedEffect(big) {
        // 动画的方式慢慢到目标值
        anim.animateTo(
            if (big) 250.dp else 50.dp,
            TweenSpec(easing = LinearEasing, durationMillis = 2000)
        )
    }
    Box(
        Modifier
            .size(anim.value)
            .background(Color.Red)
            .clickable {
                big = !big
            })
}

除匀速缓动外,其他三种都是通过三阶贝塞尔曲线去实现的:

val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 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构造函数,提供设置的四个值(是两个坐标点),另外两个点是默认(0,0),(1,1)。

mEasingMode = CubicBezierEasing(0f, 0.45f, 0.45f, 1.0f)

三阶贝塞尔曲线:通过P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线,可以通过下面这个网站玩一玩贝塞尔曲线: 玩一玩贝塞尔曲线

KeyframesSpec

TweenSpec的控制的精细程度已经很不错了,但是某些场景还是不能满足。比如想在控制这个方块在某些个时间点到达某些个指定的位置,或许在具体的某个动画段先加速后减速,另一个动画片段匀速前进。这些情况TweenSpec就捉襟见肘了,不过好在Compose为我们提供了KeyframesSpec,如其名中的Keyframes,表示关键帧,可以通过控制几个关键帧来对动画过程进行控制。

举例:控制上述的方块控制,500ms时刻到100dp的位置,在1000ms时到250dp位置。其中0-500ms要匀速缓动,500ms-1000ms要先加速后减速缓动。

// 设置动画关键帧
anim.animateTo(if (big) 250.dp else 50.dp,
    keyframes {            
        durationMillis = 1000
        50.dp at 0 with LinearEasing  // 0-500ms 匀速前进 
        100.dp at 500 with FastOutSlowInEasing // 500-1000ms 先加速后减速 500ms时刻到100dp 
        250.dp at 1000 
    }
)

需要注意的是:

  1. 缓动方式的设置,作用的是从这一帧到下一帧,上述的FastOutSlowInEasing作用的是500-1000ms这一段
  2. keyframes函数是提供的简便写法,使用KeyframesSpec构造也是可以的

SnapSpec

最开始我们通过anim.snapTo()函数快速缩放到某个大小,除了这种方式,animateTo其实也可以做到,不过它需要借助SnapSpec

anim.animateTo(
    if (big) 250.dp else 50.dp,
    SnapSpec()
)

SpringSpec

上面介绍了三种TweenSpecKeyframesSpecSnapSpec,它们有个共性,它们的父类都是DurationBasedAnimationSpec,就是说它们是基于时间的动画。什么意思呢?还有不是基于时间的设置的动画,还真有?那就是弹簧模型,你可以结合现实中的物理弹簧想一下,是不是我们按压一下,无从得知多久停下来的,会弹簧的特性来决定什么时候停下来。

前面我们介绍animateTo函数时 ,其中一个参数提供了一个默认值defaultSpringSpec,它是Compose提供的弹簧动画:

 internal val defaultSpringSpec: SpringSpec<T> =
        SpringSpec(visibilityThreshold = visibilityThreshold)

自定义一种弹簧也比较简单,只需要设置三个参数:

  • 阻尼比:控制弹力大小,值越小弹力越大,默认是1(无弹力效果)
  • 刚性:弹簧的硬度,越硬越弹不动
  • 可见阈值:到某个值停止弹动,视觉上没有看到弹动,实际上可能还在微弱的弹动,设置这个阈值可以避免无用的性能损耗

比如这里设置一种高弹力、低硬度、无可见阈值的弹簧效果:

anim.animateTo(250.dp, spring(Spring.DampingRatioHighBouncy, Spring.StiffnessLow, null))

RepeatableSpec和InfiniteRepeatableSpec

RepeatableSpec支持重复执行动画,可以通过repeatable函数来实现:

fun <T> repeatable(
    iterations: Int,
    animation: DurationBasedAnimationSpec<T>,
    repeatMode: RepeatMode = RepeatMode.Restart,
    initialStartOffset: StartOffset = StartOffset(0)
): RepeatableSpec<T> =
    RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)

四个参数:

  • iterations:重复的次数
  • animation:动画效果,注意是DurationBasedAnimationSpec类型的才可以
  • repeatMode:重复的模式,比如Restart(从头开始)、Reverse(反转)
  • initialStartOffset:初始偏移,注意这里偏移的是时间,比如延迟100ms、跳过100ms

比如下面的例子:重复执行三次,使用匀速、反转、跳过前100ms的方式

anim.animateTo(
    250.dp,
    repeatable(
        3,
        TweenSpec(easing = LinearEasing, durationMillis = 2000),
        RepeatMode.Reverse,
        StartOffset(100, StartOffsetType.FastForward)  // 跳过前100ms
    )
)

Note:

如果使用反转模式,记得次数要是奇数次,由于使用的是animateTo函数,它会以目标值为最终的结果,如果使用了偶数次重复,会出现重复结束后,闪跳到最终的目标值。

如果需要无限次数的重复,需要使用InfiniteRepeatableSpec

anim.animateTo(
    250.dp,
    infiniteRepeatable(
        TweenSpec(easing = mEasingMode, durationMillis = 2000),
        RepeatMode.Reverse,
        StartOffset(100, StartOffsetType.FastForward)
    )
)

Note:

当然这里的无限次数是指不被打断的情况,由于动画是在协程里,如果协程被取消了,这个无限动画也会停下来。

results matching ""

    No results matching ""