Compose高级别API动画指南

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

前文讲了Compose中的低级别API动画,与之对应的,还有高级别API动画,同样也符合Material-Design规范。所有高级别动画 API 都是在低级别动画 API 的基础上构建而成,其对应关系如图: image.gif 接下来就对其高级别API逐个分析:

AnimatedVisibility

即可见性动画,原Google官方文档对此API有实验性标记(可能后面更新就删除了或有其余更改,代码中要用ExperimentalAnimationApi标记),到Compose历经多个版本迭代,这些API依旧坚挺在此,估计后续也不会消失了。

可见性动画,主要是为其内容的出现、消失提供动画效果。先来看下其函数定义:

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

在原生View体系动画中,如果想实现隐藏淡入淡出放大缩小等复杂效果,可能需要alpha、scale、transition等动画一起配合实现。但在Compose中,仅通过AnimatedVisibility即可实现,其参数 enter(EnterTransition) 和 exit(ExitTransition)就可以自定义达到淡入淡出的过渡效果。visibile含义就是是否可见,这个没什么好说的。modifier是修饰符,可自定义控件各种属性。content则是指的对应的子控件。从这里可以看出,关键两个参数是EnterTransition和ExitTransition,我们先来了解下它俩。

EnterTransition

Compose对其提供动画函数如下:

fadeIn:从指定的起始alpha到1f淡入。alpha默认为0f,动画规格默认使用spring(spring相关解释在后续的自定义动画中)。

slideIn:从定义的起始偏移量到IntOffset(0,0)的滑动内容。可以通过配置控制滑动方向。正x值表示从右向左滑动,负x值表示从左向右滑动。类似地,正y值和负y值分别对应向上滑动和向下滑动。

expandIn:将显示内容的范围从返回的大小扩展到完整大小。可以控制先显示哪一部分内容。默认情况下,展示内容从IntSize(0,0)至完整大小的动画,从显示内容的右下角(或RTL布局中的左下角)逐渐扩展至显示整个内容。

expandHorizontally:将显示内容的范围从返回的宽度水平扩展到整个宽度。可以控制首先显示哪一部分内容。默认情况下,展示内容从0到全宽的动画,逐渐扩展到显示整个内容。

expandVertically:将显示内容的范围从返回的高度垂直扩展到整个高度。可以控制首先显示哪一部分内容。默认情况下,展示内容从0到全高的动画,首先显示底边,然后显示其余内容。

slideInHorizontally:从定义的起始偏移量到0水平滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值表示从右向左滑动,负值表示从左向右滑动。

slideInVertically:从定义的起始偏移量到0垂直滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值意味着向上滑动,负值意味着向下滑动。

注意,slideIn、slideInVertically、slideInVertically 这种同类型只能同时存在一个。

ExitTransition

同样,Compose对其也提供了一系列动画函数:

fadeOut:从完全不透明到目标alpha淡出效果。默认情况下,内容将淡出为完全透明,动画规格也默认使用spring(spring相关解释在后续的自定义动画中)。

slideOut:从IntOffset(0,0)到定义的目标偏移量的滑动效果。可以通过配置控制方向。x值为正表示从左向右滑动,x值为负表示从右向左滑动。类似地,正y值和负y值分别对应向下滑动和向上滑动。

shrinkOut:展示内容的范围从完整大小缩小到返回的大小的效果。可以控制范围缩小动画的方向。默认情况下,展示内容从完整大小至IntSize(0,0)的动画,并朝内容的右下角(或RTL布局中的左下角)缩小。

shrinkHorizontally:展示内容从整个宽度水平缩小到返回的宽度的效果。可以控制范围缩小动画的方向。默认情况下,剪辑范围从全宽到0设置动画,并朝内容的结尾缩小。

shrinkVertically:展示内容从整个高度垂直缩小到返回的高度消失的效果。可以控制缩小动画的方向。默认情况下,剪辑范围从全高到0设置动画,并朝内容的底部缩小。

slideOutHorizontally:从0到定义的目标偏移量水平滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值表示向右滑动,负值表示向左滑动。

slideOutVertically:从0到定义的目标偏移量垂直滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值表示向下滑动,负值表示向上滑动。

以上内容说起来很抽象,看以下实例:

fun showAnim() {
    var isShow by remember { mutableStateOf(true) }
    Column(
        Modifier.size(300.dp,300.dp),
        Arrangement.Top,
        Alignment.CenterHorizontally
    ) {
        Button(
            onClick = { isShow = !isShow }
        ) {
            Text(text = if (isShow) "Hide" else "Show")
        }

        Spacer(Modifier.height(1.dp))

        AnimatedVisibility(
            visible = isShow,
            enter = slideInVertically() + fadeIn(initialAlpha = 0.1f),
            exit = slideOutVertically() + fadeOut(targetAlpha = 0.1f)
        ) {
            Image(
                painter = painterResource(id = R.drawable.icon_pdx),
                contentDescription = null,
                Modifier.fillMaxSize()
            )
        }
    }
}
复制代码

其对应效果为:

AnimatedVisibility -1.gif

可以看出,这里有淡入淡出效果的同时,还有上滑下滑效果。通过这种 + 运算符组合多个 EnterTransition 或 ExitTransition 对象(且每个对象都可自定义可选参数和行为)的方式,达到各种效果。其余方法对应效果可以自行验证。

AnimatedContent

内容大小动画,可在内容根据目标状态发生变化时,为内容添加动画效果。其构造函数如下:

@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {
        fadeIn(animationSpec = tween(220, delayMillis = 90)) with fadeOut(animationSpec = tween(90))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
) {
    val transition = updateTransition(targetState = targetState, label = "AnimatedContent")
    transition.AnimatedContent(
        modifier,
        transitionSpec,
        contentAlignment,
        content
    )
}
复制代码

这里可以看出,使用 lambda 参数并将动画反映到内容中(可能描述不太准确),默认情况下,初始内容淡出,然后目标内容淡入(即淡出后淡入)。例如以下示例:

fun showAnim() {
    var data by remember { mutableStateOf(0) }
    Column(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        Arrangement.Center,
        Alignment.CenterHorizontally
    ) {
        Button(onClick = { data++ }) {
            Text("change")
        }

        AnimatedContent(targetState = data,
        transitionSpec ={ slideInVertically({ fullHeight ->  fullHeight}) with
                slideOutVertically({height -> -height})+ fadeOut()
        }
        ) {
            Text(text = "${data}", fontSize = 36.sp)
        }
    }
}
复制代码

对应的效果为:

AnimatedContent - 1.gif

以上可见,当 targetState(这里是data) 发生变化时,content 在我们设置的动画中完成切换。

animateContentSize

此方法主要为大小变化添加动画效果。其构造函数如下:

fun Modifier.animateContentSize(
    animationSpec: FiniteAnimationSpec<IntSize> = spring(),
    finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null
): Modifier = composed(
    inspectorInfo = debugInspectorInfo {
        name = "animateContentSize"
        properties["animationSpec"] = animationSpec
        properties["finishedListener"] = finishedListener
    }
) {
    // TODO: Listener could be a fun interface after 1.4
    val scope = rememberCoroutineScope()
    val animModifier = remember(scope) {
        SizeAnimationModifier(animationSpec, scope)
    }
    animModifier.listener = finishedListener
    this.clipToBounds().then(animModifier)
}
复制代码

不难看出此函数为Modefier的一个扩展函数,示例如下:

fun showAnim() {
    var size by remember { mutableStateOf(Size(100F, 100F)) }
    Column(
        Modifier
            .fillMaxWidth()
            .fillMaxHeight(),
        Arrangement.Center,
        Alignment.CenterHorizontally
    ) {
        Box(
            Modifier
                .animateContentSize()
        ) {
            Image(
                painter = painterResource(id = R.drawable.icon_pdx),
                contentDescription = null,
                Modifier
                    .animateContentSize()
                    .size(size = size.height.dp)
            )
        }
        Spacer(Modifier.height(10.dp))
        Button(
            onClick = {
                size = if (size.height == 100F) {
                    Size(250F, 250F)
                } else {
                    Size(100F, 100F)
                }
            }
        ) {
            Text(if (size.height == 100F) "收缩" else "展开")
        }
    }
}
复制代码

此部分代码通过变量size监听状态变化实现布局大小的动画效果:

AnimContentSize - 1.gif

Crossfade

此方法主要用于在两个布局之间添加淡入淡出动画,即布局切换动画。通过切换传递给 current 参数的值,使得淡入淡出动画切换内容。其构造函数如下:

@Composable
fun <T> Crossfade(
    targetState: T,
    modifier: Modifier = Modifier,
    animationSpec: FiniteAnimationSpec<Float> = tween(),
    content: @Composable (T) -> Unit
)
复制代码

targetState和content,在这里,targetState是指定当前的布局状态,content是显示内容,Modifier修饰符在这是修饰Crossfade,animationSpec则是指定动画类型。例如以下示例:

fun showAnim() {
    var page by remember { mutableStateOf(1) }
    Column {
        Button(onClick = { page = if (page == 1) 2 else 1 }) {
            Text("变变变")
        }
        Crossfade(
            targetState = page,
            modifier = Modifier
                .size(600.dp)
                .background(if (page == 1) Color.White else Color.Blue),
            animationSpec = spring()
        ) { screen ->
            when (screen) {
                1 -> Text("Page White", fontSize = 100.sp, color = Color.Blue)
                2 -> Text("Page Blue", fontSize = 100.sp, color = Color.White)
            }
        }
    }
}
复制代码

对应效果则为:

AnimCrossfade - 1.gif

看到这里,你应该发现了,其实套路都差不多,如果你对低级别API的动画效果还有兴趣,请点这里。下一篇即是Compose的自定义动画。

猜你喜欢

转载自juejin.im/post/7107650640377020446