Compose学习笔记(九):Compose动画之中止和入场效果

这是一个Compose学习系列,记录笔者在学习Compose过程中的核心知识点:

Compose学习笔记(一):基本控件

Compose学习笔记(二):Composable和MutableState

Compose学习笔记(三)重组和无状态

Compose学习笔记(四):状态机制和重组优化

Compose学习笔记(五):derivedStateOf和remember的使用

Compose学习笔记(六):CompositionLocal的应用场景

Compose学习笔记(七):Compose动画之AnimateSpec

Compose学习笔记(八):Compose动画之DecayAnimation

本篇主要介绍还是动画部分,主要有以下内容

1.动画的中止

2.出入场动画效果实现:AnimatedVisibility


打断动画

stop()

主动打断/终止一个正在执行的动画,可以用stop函数

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        val anim = remember { Animatable(0.dp, Dp.VectorConverter) }
        val exponentDecay = exponentialDecay<Dp>()
        LaunchedEffect(key1 = Unit) {
            delay(1000)
            try {
                anim.animateDecay(1200.dp, exponentDecay)
             } catch (e: CancellationException) {
                 Log.e(TAG, "anim is CancellationException")
                 e.printStackTrace()
             }
        }
​
      LaunchedEffect(key1 = Unit) {
        delay(1300)
        // 中断动画
        anim.stop()
      }
      
      Box(
          Modifier
              .padding(0.dp, top = anim.value, 0.dp, 0.dp)
              .size(100.dp)
              .background(Color.Red)
      )
    }
}
复制代码

并且,我们在动画执行代码中去try-catch捕获异常,在被中断时是可以catch到异常的:

E/StopAnimateActivity: anim is CancellationException
W/System.err: kotlinx.coroutines.JobCancellationException: ScopeCoroutine was cancelled; job=ScopeCoroutine{Cancelled}@54d4673
复制代码

updateBounds()

这是我们通过调用stop函数主动的停止。还有一种很常见的场景,边界中断。就和现实中一个物体滑动撞到墙一样,停了下来,Compose动画也提供了这种场景的实现:updateBounds函数。

fun updateBounds(lowerBound: T? = this.lowerBound, upperBound: T? = this.upperBound)
复制代码

比如对上述示例代码设置边界:

// 更新边界,到达边界自动停止
anim.updateBounds(upperBound = 200.dp)
复制代码

需要注意的是,这种方式的停止,和stop是不一样,它并不会抛出取消异常,该种方式被视为一种正常的中止操作。

动画函数返回值

animateDecay、animateTo等函数之前只介绍了它们的参数,其实它们是有返回值的

suspend fun animateDecay(
    initialVelocity: T,
    animationSpec: DecayAnimationSpec<T>,
    block: (Animatable<T, V>.() -> Unit)? = null
): AnimationResult<T, V>
​
class AnimationResult<T, V : AnimationVector>(
    val endState: AnimationState<T, V>,  // 结束时的状态
    val endReason: AnimationEndReason    // 结束原因
)
​
enum class AnimationEndReason {
    BoundReached, // 到达边界
    Finished      // 完成
}
复制代码

通过源码,看到动画结束时的原因有两种,一种正常完成Finished,另一种到达边界BoundReached

如上述所演示的那样,当我们通过updateBounds函数设置边界,提前中止了动画后,获取结果就会是BoundReached

Transition

Transition 可管理一个或多个动画作为其子项,并在多个状态之间同时运行这些动画。

这里的状态可以是任何数据类型。在大多数情况下,可以使用自定义 enum 类型

updateTransition()

updateTransition 可创建并记住 Transition 的实例,并更新其状态。

@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T> {
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
        onDispose {
            // Clean up on the way out, to ensure the observers are not stuck in an in-between
            // state.
            transition.onTransitionEnd()
        }
    }
    return transition
}
复制代码

举个例子,实现一个方块可以变大小的同时变化透明度的动画过程:

先定义一个枚举表示状态:

enum class BoxState {
    SMALL, BIG
}
复制代码

使用updateTransition,根据状态更改方块的大小和透明度:

@Composable
@Preview
fun TransitionExample() {
    // 设定当前的状态
    var curBoxState by remember { mutableStateOf(BoxState.SMALL) }
    // 创建/更新Transition
    val transition = updateTransition(targetState = curBoxState, label = "BoxState")
    // 大小根据状态变化
    val size by transition.animateDp(label = "size") {
        if (it == BoxState.BIG) 96.dp else 48.dp
    }
    // 透明度根据状态变化
    val alpha by transition.animateFloat(label = "alpha") {
        if (it == BoxState.BIG) 0.1f else 0.9f
    }
    Box(
        Modifier
            .size(size)
            .alpha(alpha)
            .background(Color.Green)
            .clickable {
                curBoxState = if (curBoxState == BoxState.SMALL) BoxState.BIG
                else BoxState.SMALL
            })
}
复制代码

运行看最终的效果:

transition1.gif transition1.gif

预览工具查看也是非常的方便,可以看到整个过程具体时间点对应多个状态的值:

transition的预览.png

上述案例中,多个状态的初始状态是由目标状态值targetState,也就是这里的curBoxState决定的。默认初始状态与第一个目标状态是一致的。

val transition = updateTransition(targetState = curBoxState, label = "BoxState")
复制代码

那如果我们想对初始值做单独的设定,希望初始状态与第一个目标状态不同,就可以通过结合使用 updateTransitionMutableTransitionState 来实现这一点。

// 设定初始状态
val curBoxTransitionState = remember { MutableTransitionState(BoxState.SMALL) }
// 设定目标状态
curBoxTransitionState.targetState = BoxState.BIG
// 创建/更新Transition
val transition = updateTransition(targetState = curBoxTransitionState, label = "BoxState")
复制代码

结合AnimationSpec

如果想对某个点状态的动画过程做细节的定制,也可以和之前之前学习的AnimationSpec结合使用:

animateDp函数API中,为我们提供这样一个参数transitionSpec,它是一个FiniteAnimationSpec对象

inline fun <S> Transition<S>.animateDp(
    noinline transitionSpec: @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<Dp> = {
        spring(visibilityThreshold = Dp.VisibilityThreshold)
    },
    label: String = "DpAnimation",
    targetValueByState: @Composable (state: S) -> Dp
): State<Dp> =
    animateValue(Dp.VectorConverter, transitionSpec, label, targetValueByState)
复制代码

所以当我们想对上面的方块的大小状态进行弹性动画,就可以这样做:

val size by transition.animateDp(
    {
        if (curBoxTransitionState.currentState == BoxState.SMALL
            && curBoxTransitionState.targetState == BoxState.BIG
        ) {
            tween(2000)
        } else spring()
    },
    label = "size"
) {
    if (it.targetState == BoxState.BIG) 200.dp else 48.dp
}
复制代码

AnimatedVisibility

如果想对一个view的显隐状态变化做动画,或者说是出入场动画,就可以用AnimatedVisibility

@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
复制代码

给我们提供了很多扩展方法,便于使用,比如Column内的view

@Composable
fun ColumnScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandVertically(),  // 
    exit: ExitTransition = fadeOut() + shrinkVertically(),   //
    label: String = "AnimatedVisibility",
    content: @Composable AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
复制代码

其中最重要的两个参数就是enterexit,可以实现入场和出场动画效果的配置。下面详细介绍下入场enter,出场exit和它相反的。

比如,实现一个Box的淡入效果,使用fadeIn

var visible by remember {
    mutableStateOf(false)
}
// 淡入使用fadeIn,initialAlpha: 初始透明度
AnimatedVisibility(visible = visible, enter = fadeIn(initialAlpha = 0.2f)) {
    BoxView()
}
复制代码

比如,实现滑动入场效果,使用slideIn

// 滑动入场使用slideIn,initialOffset:设置出场位置
AnimatedVisibility(
    visible = visible,
    enter = slideIn(initialOffset = { IntOffset(-it.width, -it.height) })
) {
    BoxView()
}
复制代码

比如,展开,裁剪入场效果,使用expandIn

 // 展开,裁剪效果入场,expandFrom: 展开的方向, initialSize: 展开的初始位置,clip: 是否裁剪,默认是true
 AnimatedVisibility(
     visible = visible,
     enter = expandIn(
         tween(5000),
         expandFrom = Alignment.TopStart,
         initialSize = { IntSize(it.width / 2, it.height / 2) },
         clip = true
     )
 ) {
     BoxView()
 }
复制代码

再比如设置缩放入场效果,使用scaleIn

// initialScale:初次缩放值得,transformOrigin: 设置缩放中心,默认是中心
AnimatedVisibility(
    visible = visible,
    enter = scaleIn(
        initialScale = 0f, transformOrigin = TransformOrigin(0f, 0f)
    )
) {
    BoxView()
}
复制代码

如果想实现更复杂的效果,比如对上述的多个入场效果进行叠加,也非常简单,一个+就可以解决:

// 叠加效果:淡入+裁剪展开
AnimatedVisibility(
    visible = visible,
    enter = fadeIn(tween(5000), initialAlpha = 0.1f) + expandIn(tween(3000), expandFrom = Alignment.TopStart)
) {
    BoxView("多种效果叠加入场")
}
复制代码

需要注意的是,如果+前后使用了重复的效果,并不会叠加,而是优先使用前者,从源码实现可以看出:

operator fun plus(enter: EnterTransition): EnterTransition {
    return EnterTransitionImpl(
        TransitionData(
            fade = data.fade ?: enter.data.fade,
            slide = data.slide ?: enter.data.slide,
            changeSize = data.changeSize ?: enter.data.changeSize,
            scale = data.scale ?: enter.data.scale
        )
    )
}
复制代码

参考

updateTransition

猜你喜欢

转载自juejin.im/post/7105570444278759454
今日推荐