Jetpack Compose 从入门到入门(七)

本篇进入Compose 动画部分。

1.动画预览

在本系列第一篇中我们提到过,@Preview可以帮我们实现UI的预览功能,简单的交互和播放动画。

在Android Studio Bumblebee(大黄蜂)中你可以开启动画的预览,但是只支持少部分API。

在这里插入图片描述
前几天Android Studio 稳定版更新到了Chipmunk(花栗鼠),开始支持 animatedVisibility 的动画预览,这里也建议你将 Compose 升至 1.1.0 或更高版本,可以体验更完整的内容。

提示:本篇使用Compose版本为1.1.0(对应 Kotlin版本为 1.6.10)。会涉及一些实验性API,可能后面会有变动甚至取消。

简单说一先动画预览的功能。当检测到预览的ui中有支持预览的动画时,会出现一个在这里插入图片描述 图标。点击这个图标后,就可以看到具体每个动画的运动曲线,我们可以在面板上拖动、快进或放慢动画。逐帧预览过渡效果。

举个小例子,我在页面上放置一个OutlinedTextField输入框,我们看一下实际的动画预览效果。

在这里插入图片描述

是不是很强大,很便捷。好了,下面正式开始动画API部分了。

2. 高级别API

高级别api就是对基础api的封装,便于我们更好的使用。

1. AnimatedVisibility(实验性)

看名字就知道,它是一个显示隐藏的动画。

@ExperimentalAnimationApi
@Composable
fun <T> Transition<T>.AnimatedVisibility(
    visible: (T) -> Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) = AnimatedEnterExitImpl(this, visible, modifier, enter, exit, content)
  • visible: 显示还是隐藏,显示执行enter,隐藏执行exit。
  • enter:显示动画,默认是从左上角开始水平和垂直方向同时展开时淡入。目前EnterTransition有四种类型:
  1. fade: fadeIn
  2. scale: scaleIn
  3. slide: slideIn, slideInHorizontally, slideInVertically
  4. expand: expandIn, expandHorizontally, expandVertically
  • exit:消失动画,默认是收缩(与显示相反)时淡出。ExitTransition如下:
  1. fade: fadeOut
  2. scale: scaleOut
  3. slide: slideOut, slideOutHorizontally, slideOutVertically
  4. shrink: shrinkOut, shrinkHorizontally,shrinkVertically

多个动画效果我们可以使用 + 运算符进行组合,使用起来很方便。具体这些动画的效果我们可以参考官方文档的示例动图:EnterTransition、ExitTransition 示例

需要注意的是,RowColumn下的Scope有对应的AnimatedVisibility拓展方法,所以默认动画略有不同。以Row举例:

@Composable
fun RowScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandHorizontally(),
    exit: ExitTransition = fadeOut() + shrinkHorizontally(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

Row中的显示隐藏动画是expandHorizontallyshrinkHorizontally,所以是水平方向从左到右展开时淡入,从右到左收缩时淡出。Column就是垂直方向过渡。这点我们在使用时需要注意。

AnimatedVisibility 还提供了传入 MutableTransitionState的变体方法。该属性有助于观察动画状态。官方demo:

// Create a MutableTransitionState<Boolean> for the AnimatedVisibility.
val state = remember {
    
    
    MutableTransitionState(false).apply {
    
    
        // Start the animation immediately.
        targetState = true
    }
}
Column {
    
    
    AnimatedVisibility(visibleState = state) {
    
    
        Text(text = "Hello, world!")
    }

    // Use the MutableTransitionState to know the current animation state
    // of the AnimatedVisibility.
    Text(
        text = when {
    
    
            state.isIdle && state.currentState -> "Visible"
            !state.isIdle && state.currentState -> "Disappearing"
            state.isIdle && !state.currentState -> "Invisible"
            else -> "Appearing"
        }
    )
    Button(
        onClick = {
    
    
            state.targetState = !state.targetState
        }
    ) {
    
    
        Text("Change")
    }
}

效果图:

在这里插入图片描述

为子项添加进入和退出动画效果

AnimatedVisibility中的内容可以使用animateEnterExit修饰符为每个子项指定不同的动画行为。我稍微修改了一下官方的demo:

val visible = remember {
    
     false }
Column(modifier = Modifier.fillMaxWidth()) {
    
    
    AnimatedVisibility(
        visible = visible,
        enter = fadeIn(),
        exit = fadeOut()
    ) {
    
    
        Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
    
    
            Box(
                Modifier
                    .align(Alignment.Center).padding(bottom = 150.dp)
                    .animateEnterExit(
                        enter = slideInVertically(),
                        exit = slideOutVertically()
                    )
                    .sizeIn(minWidth = 150.dp, minHeight = 150.dp)
                    .background(Color.Red)
            )
            Box(
                Modifier
                    .align(Alignment.Center).padding(top = 150.dp)
                    .animateEnterExit(
                        enter = slideInHorizontally(),
                        exit = slideOutHorizontally()
                    )
                    .sizeIn(minWidth = 150.dp, minHeight = 150.dp)
                    .background(Color.Blue)
            )
        }
    }
}

页面整体是灰色背景的,其中有一红一蓝两个方块居中放置。灰色背景是淡入淡出,红蓝方块是滑动效果。效果如下:

在这里插入图片描述

如果你不需要动画效果,可以设置EnterTransition.NoneExitTransition.None

添加自定义动画

通过AnimatedVisibilitytransition属性访问底层Transition实例,就可以添加自定义动画,这些动画将与AnimatedVisibility的进入和退出动画同时运行。

AnimatedVisibility(
    visible = visible,
    enter = fadeIn(),
    exit = fadeOut()
) {
    
     
    val background by transition.animateColor {
    
     state ->
        if (state == EnterExitState.Visible) Color.Blue else Color.Red
    }
    Box(modifier = Modifier.size(128.dp).background(background))
}

效果如下:

在这里插入图片描述
关于Transition,我们后面说低级动画API时会说明。

2. AnimatedContent(实验性)

AnimatedContent会在内容目标状态发生变化时,为内容添加动画效果。

@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {
    
    
        fadeIn(animationSpec = tween(220, delayMillis = 90)) +
            scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) with
            fadeOut(animationSpec = tween(90))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
) {
    
    ...}

根据源码我们可以看到默认动画效果是淡出后同时放大淡入。

  • targetState:目标状态,当它变化时会触发动画。
  • transitionSpec:过渡的动画效果。我们可以利用AnimatedContentScope中的initialStatetargetState等属性,来自定义动画效果。

下面看一个数字变换的例子:

var count by remember {
    
     mutableStateOf(0) }
Column(modifier = Modifier.fillMaxWidth().padding(10.dp)) {
    
    
    AnimatedContent(
        targetState = count,
        transitionSpec = {
    
    
            // 将当前的数字与之前的数字进行比较。
            if (targetState > initialState) {
    
    
                // 如果目标数字较大,它会向上滑动并淡入,而初始(较小)数字则向上滑动并淡入。
                slideInVertically {
    
     height -> height } + fadeIn() with
                        slideOutVertically {
    
     height -> -height } + fadeOut()
            } else {
    
    
                // 如果目标数字较小,则它会下滑并淡入,而初始数字则下滑并淡入。
                slideInVertically {
    
     height -> -height } + fadeIn() with
                        slideOutVertically {
    
     height -> height } + fadeOut()
            }.using(
                // 禁用剪切,因为要显示在边界之外。
                SizeTransform(clip = false)
            )
        }
    ) {
    
     targetCount ->
        Text(text = "$targetCount", fontSize = 33.sp)
    }
    Button(onClick = {
    
     count++ }) {
    
    
        Text("Add")
    }
}

效果如下:

在这里插入图片描述

代码中使用using传入一个SizeTransform,禁止将内容裁剪为组件大小。同时我们可以使用SizeTransform中的initialSize, targetSize属性来定义过渡中的大小变化。

3. animateContentSize

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

animateContentSizeModifier的一个扩展方法,可以在内容大小发生变化时添加动画过渡效果。此方法使用简单,这里就不过多的说明了。

4. Crossfade

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

Crossfade 在布局切换时添加淡入淡出动画。用法与AnimatedContent类似。

3. 低级别动画API

1. animateXXXAsState

animateXXXAsState 方法是 Compose中最简单的动画API,用于为单个值添加动画效果。您只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。

Compose 为 Float、Color、Dp、Size、Offset、Rect、Int、IntOffset 和 IntSize 提供开箱即用的 animate*AsState 方法。通过为接受通用类型的 animateValueAsState 提供 TwoWayConverter,您可以轻松添加对其他数据类型的支持。

在这里插入图片描述

例如animateIntAsState源码如下:

@Composable
fun animateIntAsState(
    targetValue: Int,
    animationSpec: AnimationSpec<Int> = intDefaultSpring,
    finishedListener: ((Int) -> Unit)? = null
): State<Int> {
    
    
    return animateValueAsState(
        targetValue, Int.VectorConverter, animationSpec, finishedListener = finishedListener
    )
}

private val intDefaultSpring = spring(visibilityThreshold = Int.VisibilityThreshold)

默认的动画效果都是spring,它是一种弹簧(弹性)动画。

@Stable
fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy,
    stiffness: Float = Spring.StiffnessMedium,
    visibilityThreshold: T? = null
)

不过dampingRatio默认值是1f,stiffness默认是1500f,所以实际并无明显的弹性和摆动。

2. updateTransition

animateXXXAsState适合单个属性变化的动画,如果是同时执行多个动画,可以使用updateTransition

@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T> {
    
    
    val transition = remember {
    
     Transition(targetState, label = label) }
    transition.animateTo(targetState)
    DisposableEffect(transition) {
    
    
        onDispose {
    
    
            transition.onTransitionEnd()
        }
    }
    return transition
}
  • targetState:目标状态,变化时会触发动画。
  • label:动画预览时的动画名称,用于区分动画。

下面直接照搬官方Demo,说明一下如何使用updateTransition类似。

// 定义枚举类型
enum class BoxState {
    
    
    Collapsed,
    Expanded
}

// 定义保存动画值的对象
private class TransitionData(
    color: State<Color>,
    size: State<Dp>
) {
    
    
    val color by color
    val size by size
}

页面上创建一个Box,它的大小背景色来自动画值,点击按钮更新状态。

@Composable
private fun updateTransitionDemo() {
    
    
    Column(modifier = Modifier.fillMaxWidth().padding(10.dp)) {
    
    
        var currentState by remember {
    
     mutableStateOf(BoxState.Collapsed) }
        val transitionData = updateTransitionData(currentState)
        Box(
            modifier = Modifier
                .background(transitionData.color)
                .size(transitionData.size)
        )
        Button(onClick = {
    
    
            currentState = if (currentState == BoxState.Collapsed) {
    
    
                BoxState.Expanded
            } else {
    
    
                BoxState.Collapsed
            }
        }) {
    
    
            Text(text = "Update")
        }
    }
}

这里核心是updateTransitionData方法:

@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
    
    
    val transition = updateTransition(boxState)
    val color = transition.animateColor(label = "color") {
    
     state ->
        when (state) {
    
    
            BoxState.Collapsed -> Color.Gray
            BoxState.Expanded -> Color.Red
        }
    }
    val size = transition.animateDp(label = "dp") {
    
     state ->
        when (state) {
    
    
            BoxState.Collapsed -> 64.dp
            BoxState.Expanded -> 128.dp
        }
    }
    return remember(transition) {
    
     TransitionData(color, size) }
}

updateTransition可创建并记住Transition的实例,并更新其状态。通过transition可以使用某个animateXXX 扩展方法来定义此过渡效果中的子动画。为每个状态指定目标值。(Transition同时也有AnimatedVisibilityAnimatedContent的拓展方法。)

具体实现的效果就是展开时128dp的红色方块,收起就是64dp的灰色方块。具体效果如下:

在这里插入图片描述

3. rememberInfiniteTransition

用来创建一个无限循环的动画。

比如我们创建一个红绿色过渡的无限循环动画,代码如下:

val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(
    initialValue = Color.Red,
    targetValue = Color.Green,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    )
)

Box(Modifier.fillMaxSize().background(color))

篇幅有限,我们下一篇再介绍Animatable与自定义动画部分。

参考

猜你喜欢

转载自blog.csdn.net/qq_17766199/article/details/124936353