《Jetpack Compose系列学习》-20 Compose中的简单动画

931 阅读8分钟

可见性动画

在开发过程中一定写过这样的需求:“当符合某个条件的时候显示某个控件,否则就隐藏该控件”。对于这种需求,一般通过控制控件的visible状态或alpha值来实现。但是在Compose中我们可以使用AnimatedVisibility可组合项来实现。看个案例:

@ExperimentalAnimationApi
@Composable
fun EasyAnimation() {
    val visible = remember {
        mutableStateOf(true)
    }
    Column(modifier = Modifier
        .size(360.dp)
        .padding(10.dp)) {
            Button(onClick = {visible.value = !visible.value}) {
            Text("可见性动画")
        }
        AnimatedVisibility(visible = visible.value) {
            Text(text = "验证可见性动画", modifier = Modifier.size(150.dp))

        }

    }
}

1.gif

通过remember创建一个State来记住当前显示的状态,然后我们创建了一个布局,里面包含了一个按钮和文字,点击按钮的时候修改显示状态的State。如果想让某个布局实现可见性动画,就需要使用AnimatedVisibility将这个布局包裹起来,然后对其设置可见性。这里需要注意的是,AnimatedVisibility也是一个实验性API,所以使用的时候我们需要添加ExperimentalAnimationApi注解。效果如上。

直接使用Preview预览是无法进行交互的,即点击按钮是没用的,我们可以按照下图箭头所示的按钮来打开Preview的互动模式,然后点击按钮就可以看到可见性动画的效果了。

image.png

我们再来看看AnimatedVisibility的方法定义:

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibility(
    visible: Boolean, // 当前可见性
    modifier: Modifier = Modifier, // 修饰符
    enter: EnterTransition = fadeIn() + expandIn(), // 进入动画,默认淡入
    exit: ExitTransition = shrinkOut() + fadeOut(), // 关闭动画,默认缩小淡出
    initiallyVisible: Boolean = visible, // 控件是否对第一个外观进行动画处理,默认为匹配visible
    content: @Composable () -> Unit // 子控件
) {
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

AnimatedVisibility的参数:第一个参数是visible, 类型Boolean,控制当前内容是否可见,根据实际需求设定即可;第二个参数modifier,是修饰符;第三个参数是enter,类型是EnterTransition,控制显示的动画,默认动画为淡入;第四个参数是exit,类型是ExitTransition,用来控制关闭动画,默认是缩小淡出;第五个参数是initiallyVisible,类型Boolean,控制是否对第一个外观进行动画处理,默认是我们设置的visible值。

其中两个参数我们之前没遇到过:enter和exit。先看看enter,默认值是fadeIn() + expandIn(), 这种写法之前没见过,来看看EnterTransition源码:

@ExperimentalAnimationApi
@Immutable
sealed class EnterTransition {
    internal abstract val data: TransitionData

    /**
     * 自核不同的输入动画
     */
    @Stable
    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
            )
        )
    }
    // 省略...

EnterTransition是一个密封类,有一个plus方法,并用operator修饰,表示运算符的重载,重载了“+”号,可以用来组合不同的输入动画。

再看看ExitTransition源码:

@ExperimentalAnimationApi
@Immutable
sealed class ExitTransition {
    internal abstract val data: TransitionData

    /**
     * 组合不同的退出过渡动画
     */
    @Stable
    operator fun plus(exit: ExitTransition): ExitTransition {
        return ExitTransitionImpl(
            TransitionData(
                fade = data.fade ?: exit.data.fade,
                slide = data.slide ?: exit.data.slide,
                changeSize = data.changeSize ?: exit.data.changeSize
            )
        )
    }
    // 省略...

可以看出,它和EnterTransition相似,都是密封类,且都对“+”号运算符进行了重载,可以用来组合不同的动画。

来看看Compose为EnterTransition和ExitTransition提供了那些可组合的动画

先看下EnterTransition提供的动画组合有哪些:

  • fadeIn:从指定的起始alpha到1f淡入。alpha默认值是0f。
  • slideIn:从定义的起始偏移量到IntOffset(0, 0)滑动内容,x为正值时时从右向左滑动,否则是从左向右滑动;类似y正值时是向上滑动,否则向下滑动。
  • expandIn:将显示内容的剪辑范围从返回的大小扩展到完整大小。可控制首先显示哪一部分内容。默认剪辑范围从IntSize(0, 0)至完整大小设置动画,从显示内容的右下角逐渐扩展到显示整个内容。
  • expandHorizontally:将显示内容的剪辑范围从返回的宽度水平扩展到整个宽度。默认情况下,剪辑范围从0到全宽设置动画,逐渐扩展到显示整个内容。
  • expandVertically:将显示内容的剪辑范围从返回的高度垂直扩展到整个高度。可以控制首先显示哪一部分内容弄。默认剪辑范围从0到全高设置动画,首先显示底边,然后显示其余内容。
  • slideInHorizontally:从定义的起始偏移量到0水平滑动内容。可以通过配置控制幻灯片的方向。正值表示从右向左滑动,否则从左向右滑动。
  • slideInVertically:从定义的起始偏移量到0垂直滑动内容。可以通过配置控制幻灯片的方向。正值表示向上滑动,否则向下滑动。

ExitTransition提供的动画组合有哪些:

  • fadeOut:从完全不透明到目标alpha淡出。默认内容将淡出为完全透明。
  • slideOut:从定义的起始偏移量到IntOffset(0, 0)滑动内容,x为正值时时从左向右滑动,否则是从右向左滑动;类似y正值时是向下滑动,否则向上滑动。
  • shrinkOut:将小时内容的剪辑范围从完整大小缩小到返回的大小。可以控制范围缩小动画的方向。默认剪辑范围从完整大小至IntSize(0, 0)设置动画,并朝内容的右下角缩小。
  • shrinkHorizontally:将小时内容的剪辑范围从整个宽度水平缩小至返回的宽度。可以控制范围缩小动画的方向。默认剪辑范围从全宽到0设置动画,并朝内容的结尾缩小。
  • shrinkVertically:将小时内容的剪辑范围从整个宽度垂直缩小至返回的高度。可以控制范围缩小动画的方向。默认剪辑范围从全高到0设置动画,并朝内容底部缩小。
  • slideOutHorizontally:从0到定义的目标偏移量水平滑动内容。可以通过配置控制幻灯片的方向。正值表示向右滑动,否则向左滑动。
  • slideOutVertically:从0到定义的目标偏移量垂直滑动内容。可以通过配置控制幻灯片的方向。正值表示向下滑动,否则向上滑动。

对比EnterTransition和ExitTransition的可组合动画,我们发现都是成对出现的,而且动画效果相反,因此我们可以随意组合。看个例子:

@ExperimentalAnimationApi
@Composable
fun EasyAnimation() {
    val visible = remember {
        mutableStateOf(true)
    }
    Column(modifier = Modifier
        .size(360.dp)
        .padding(10.dp)) {
            Button(onClick = {visible.value = !visible.value}) {
            Text("可见性动画")
        }
        AnimatedVisibility(
            visible = visible.value,
            enter = slideIn({ IntOffset(400, 400)}) + expandIn(),
            exit = slideOut({ IntOffset(400, 400)}) + shrinkOut()
        ) {
            Text(text = "验证可见性动画", modifier = Modifier.size(150.dp))
        }

    }
}

我们设置了enter和exit动画,可以在实际开发中随意组合,达到预期效果。

布局大小动画

我们经常看到朋友圈的阅读全文功能,当文字超过固定行数后在文字最后会显示“全文”,点击会展开显示完整的内容。

image.png

在Android View中要是实现这种效果,比较麻烦,我们需要自定义TextView去实现,但是在Compose中只需要调用Modifier的扩展方法animateContentSize即可,先看看这个扩展方法的定义:

public fun Modifier.animateContentSize(
animationSpec: FiniteAnimationSpec<IntSize> = spring(), 
finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null 
): Modifier

当其子修改器更改大小时,此修改器将为其自身设置动画尺寸。这使得父修改器可以观察到平滑的大小变化,从而导致整体上连续的视觉变化。animateContentSize扩展方法中有两个参数:animationSpec,用于动画尺寸变化的动画,默认为spring;finishedListener是一个动画回调,当内容更改动画完成时进行回调,回调时会将初始值和目标值同时传回来。但需要注意的是,如果动画中断,则初始值将是中断点的大小。

回头我们看看上面说的按个“全文”展开怎么去实现:

val expend = remember {
    mutableStateOf(false)
}

Column(modifier = Modifier
    .size(360.dp)
    .padding(10.dp)) {

    Text(
        text = "直接抒情能更好的更强烈的表达自己内心的情感,喜怒哀乐流露于外,给人的感觉更直观,心中有话不吐不快," +
                "胸中有情不抒不快。依附性抒情就是把自己的情感依附于其他事物上进行抒情,这种抒情,比较间接,也比较委婉和含蓄。",
        fontSize = 16.sp,
        textAlign = TextAlign.Justify,
        overflow = TextOverflow.Ellipsis,
        modifier = Modifier.animateContentSize(),
        maxLines = if (expend.value) Int.MAX_VALUE else 2
    )
    Text(if (expend.value) "收起" else "全文", color = Color.Blue, modifier = Modifier.clickable {
        expend.value = !expend.value
    })

}

2.gif

大家在使用布局大小变化动画的时候可以根据实际需求修改动画规格,并且可以通过回调监听布局的变化。

布局切换动画

很多场景会用到布局切换动画,如我们之前说过的底部导航栏上面的页面切换。Compose中布局切换动画为Crossfade,它可使用淡入淡出动画在两个布局之间添加动画效果。通过切换传递给current参数的值,可以给内容添加淡入淡出的动画。先看看Crossfade的方法定义:

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

一个四个参数,第一个参数是targetState,是个泛型,代表目标布局状态的键,每次更改键都会触发动画,用旧键调用的布局将淡出,而用新键调用的布局将淡入;第二个参数是modifier;第三个参数是animationSpec,类型是FiniteAnimationSpec,也是动画规格,默认为tween(),tween()创建了一个配置给定的持续时间、延迟和缓和曲线的补间动画;第四个参数是content,类型是@Composable (T) -> Unit,第一个采纳数传入的泛型在这里通过参数实现回调。看个例子:

val tabs = MainTabs.values() // tab数据
var position by remember { mutableStateOf(MainTabs.ONE)}
Scaffold(
    backgroundColor = Color.Yellow, // 背景色
    bottomBar = { // bottomBar
        BottomNavigation(
            backgroundColor = MaterialTheme.colors.primary,
            contentColor = Color.Red
        ) {
            tabs.forEach { tab ->
                BottomNavigationItem(
                    modifier = Modifier.background(MaterialTheme.colors.primary),
                    icon = { Icon(painterResource(id = tab.icon), contentDescription = null) },
                    label = { Text(tab.tabName) },
                    selected = tab == position,
                    onClick = {
                        position = tab
                    },
                    alwaysShowLabel = false,
                    selectedContentColor = Color.Green,
                    unselectedContentColor = Color.Red
                )
            }
        }
    }
) {
    Crossfade(targetState = position) { screen -> // 切换动画
        when(screen) { 
            MainTabs.ONE -> One()
            MainTabs.TWO -> Two()
            MainTabs.THREE -> Three()
            MainTabs.FOUR -> Four()
        }
    }
}

只需要将之前的布局切换代码外套一层Crossfade,即可实现布局切换动画。运行就可以看到淡入淡出的动画效果了。下一篇会学习属性动画等其它动画。代码已上传:github.com/Licarey/com…