Jetpack Compose 动画 API: AnimatedVisibility & AnimatedContent

Jetpack Compose 的动画相关的 API 数量众多,分为低级别 API 和高级别 API,其中高级别 API 便于使用者针对具体场景开箱即用 ,其中最常用的当属 AnimatedVisibilityAnimatedContent 这两个了。


1. AnimatedVisibility


AnimatedVisibility 顾名思义是用动画的方式改变 UI 元素的 Visibility,具体来说就是针对让其内部的 Composable 以动画的形式进入或退出屏幕

AnimatedVisibility 可以接受一个 visible 的 boolean 参数,控制内部元素的显示或隐藏

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilitySample() {
    var editable by remember { mutableStateOf(true) }

    Column(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "AnimatedVisibility",
            style = MaterialTheme.typography.h6
        )

        AnimatedVisibility(visible = editable) {
            Surface(
                color = Color.Yellow,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .align(Alignment.CenterHorizontally)
                    .padding(8.dp)
            ) {}
        }

        Button(
            onClick = { editable = !editable },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Toggle")
        }
    }
}

看上面的例子,点击 Button 时,editable 发生变化, AnimatedVisibility 内的 Surface 显示或隐藏,同时伴随动画,效果如下:
请添加图片描述

MutableTransitionState

AnimatedVisibility 还有一个重载的方法, 接收一个 MutableTransitionState 类型的 visibleState 参数,上面的代码还可以写成下面这样

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityStateSample() {
    val state = remember {
        MutableTransitionState(false).apply {
            targetState = true
        }
    }

    Column(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "AnimatedVisibilityState",
            style = MaterialTheme.typography.h6
        )

        AnimatedVisibility(visibleState = state) {
            Surface(
                color = Color.Yellow,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .align(Alignment.CenterHorizontally)
                    .padding(8.dp)
            ) {
                Text(
                    text = state.getAnimationState().toString()
                )
            }
        }

        Button(
            onClick = { state.targetState = !state.currentState },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Toggle")
        }
    }
}

MutableTransitionState 的定义如下,主要有 currentState 和 targetState 两个成员组成,

class MutableTransitionState<S>(initialState: S) {

    var currentState: S by mutableStateOf(initialState)
        internal set

    var targetState: S by mutableStateOf(initialState)

    val isIdle: Boolean
        get() = (currentState == targetState) && !isRunning

    internal var isRunning: Boolean by mutableStateOf(false)
}

前面的例子中,MutableTransitionState 的初始状态 currentState 为 false,目标状态 targetState 为 true,状态差可以实现 AnimatedVisibility 上屏时立即执行动画的效果。
请添加图片描述

MutableTransitionState 的 currentStateisIdle 可以暴露当前动画的执行状态给外面参考,我们可以定义一个枚举表示动画的状态

enum class AnimState {
    VISIBLE,
    INVISIBLE,
    APPEARING,
    DISAPPEARING
}

fun MutableTransitionState<Boolean>.getAnimationState(): AnimState {
    return when {
        this.isIdle && this.currentState -> AnimState.VISIBLE
        !this.isIdle && this.currentState -> AnimState.DISAPPEARING
        this.isIdle && !this.currentState -> AnimState.INVISIBLE
        else -> AnimState.APPEARING
    }
}

EnterTransition & ExitTransition

AnimatedVisibility 可以通过 enterexit 参数指定动画样式,enter 和 exit 分别制定一个 EnterTransition 和一个 ExitTransition 。

例如我们可以指定 fadeIn 和 fadeOut 的动画效果:

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityEnterExitSample() {
    val state = remember {
        MutableTransitionState(false).apply {
            targetState = true
        }
    }

    Column(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "AnimatedVisibilityState",
            style = MaterialTheme.typography.h6
        )

        AnimatedVisibility(
            visibleState = state,
            enter = fadeIn(),
            exit = fadeOut()
        ) {
            Surface(
                color = Color.Yellow,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .align(Alignment.CenterHorizontally)
                    .padding(8.dp)
            ) {
                Text(text = state.getAnimationState().toString())
            }
        }

        Button(
            onClick = { state.targetState = !state.currentState },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Toggle")
        }
    }
}

请添加图片描述

2. AnimatedContent


AnimatedContent 和 AnimatedVisibility 类似,都是通过动画完成 content 内部的状态变化,AnimatedVisibility 是控制显隐,AnimatedContent 是控制切换:

@ExperimentalAnimationApi
@Composable
fun AnimatedContentCounterDefault() {
    Column {
        var count: Int by remember { mutableStateOf(0) }

        AnimatedContent(targetState = count) { targetCount ->
            Text(
                text = "$targetCount",
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h2,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(onClick = { count++ }) {
                Text("PLUS")
            }

            Button(onClick = { count-- }) {
                Text("MINUS")
            }
        }
    }
}

如上,AnimatedContent 接收一个 targetState 参数,同时 content 基于 targetContent 构建新的 UI,targetState 的不同导致 content 的不同,当 targetState 发生变化时,content 在动画中完成切换。
targetState 可以是任意类型的值,上面例子中,我们基于 Int 型的 count 生成新的 content,点击 PLUS 和 MINUS 之后,content 的 Text 在动画中完成切换:请添加图片描述

ContentTransform

切换动画可以通过 transitionSpec 参数设置一个 ContentTransform, ContentTransform 可以通过 with 中缀操作符,组合 EnterTransition 和 ExitTransition 而成。

ContentTransform = EnterTransition with ExitTransition

  • EnterTransition:新的 content 的进入时的动画
  • ExitTransition: 旧的 content 退出时的动画

例如,我们使用 ContentTransform 实现一个 Slide 效果的切换动画:

  • 从右到左的切换,并伴随淡入淡出效果:
    • EnterTransion:使用 slideInHorizontally,初始位置 initialOffsetX = width
    • ExitTransition:使用 slideOutHorizontally,目标位置 targetOffsetX = -width
slideInHorizontally({ width -> width }) + fadeIn() 
 with slideOutHorizontally({ width -> -width }) + fadeOut()
  • 从左到右的切换,并伴随淡入淡出效果:
    • EnterTransion:使用 slideInHorizontally,初始位置 initialOffsetX = -width
    • ExitTransition:使用 slideOutHorizontally,目标位置 targetOffsetX = width
slideInHorizontally({ width -> -width }) + fadeIn()
 with slideOutHorizontally({ width -> width }) + fadeOut()

我们应用上述 transitionSpec 后的代码:

@ExperimentalAnimationApi
@Composable
fun AnimatedContentCounterCustom() {
    Column {
        var count: Int by remember { mutableStateOf(0) }

        AnimatedContent(
            targetState = count,
            transitionSpec = {
                val isPlus = targetState > initialState
                if (isPlus) {
                    slideInHorizontally({ width -> width }) + fadeIn() with slideOutHorizontally({ width -> -width }) + fadeOut()
                } else {
                    slideInHorizontally({ width -> -width }) + fadeIn() with slideOutHorizontally({ width -> width }) + fadeOut()
                }.using(
                    SizeTransform(clip = false)
                )
            }
        ) { targetCount ->
            Text(
                text = "$targetCount",
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h2,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(onClick = { count++ }) {
                Text("PLUS")
            }

            Button(onClick = { count-- }) {
                Text("MINUS")
            }
        }
    }
}

点击 PLUS 时,数字从左到右移入,点击 MINUS 时,数字从右到左移出,效果如下:
请添加图片描述

SizeTransform

transitionSpec 中指定 ContentTransform 的同时,还可以通过 using 中缀添加 SizeTransform 动画

SizeTransform = EnterTransition with ExitTransition using SizeTransform

SizeTransform 中或获取旧 content 和新 content 的 size,并通过 keyframes 定义动画的执行规则:在何时应该多大且总的持续时长是多少

fadeIn() with fadeOut() using SizeTransform { initialSize, targetSize ->
	keyframes {
		// at :在 250 时刻应有的大小
		IntSize(initialSize.width, initialSize.height) at 250

		// durationMillis :动画的执行总时间
		durationMillis = 500
	}
}

添加 SizeTransform 之后的全部代码如下:

@ExperimentalMaterialApi
@ExperimentalAnimationApi
@Composable
fun AnimatedContentExpandableTextSample() {
    var expanded by remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.primary,
        onClick = { expanded = !expanded },
        modifier = Modifier.padding(8.dp)
    ) {
        AnimatedContent(
            targetState = expanded,
            transitionSpec = {
                fadeIn() with fadeOut() using SizeTransform { initialSize, targetSize ->
                    keyframes {
                        IntSize(initialSize.width, initialSize.height) at 250
                        durationMillis = 500
                    }
                }
            }
        ) { targetExpanded ->
            if (targetExpanded) {
                Text(
                    text = "Expanded",
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(100.dp)
                )
            } else {
                Text(
                    text = "Not Expanded",
                    modifier = Modifier.wrapContentSize()
                )
            }
        }
    }
}

添加 SizeTransform 之后,点击按钮后的变化过程中,content 面积也会出现过度动画,效果如下:
请添加图片描述


3. 总结一下


AnimatedVisibility 和 AnimatedContent 是最常用的 Composable 动画 API ,它们向其他 Layout 元素一样都是 Composable 函数,动画效果作用于其内部的子 Composable:

  • AnimatedVisibility 用来实现 content 的动画显隐,通过 enter 和 exit 可以自定义动画效果
  • AnimatedContent 用来实现 content 的动画切换,等价于旧 content 的隐藏 + 新 content 的显示 ,通过 ContentTransform 和 SizeTransform 可以自定义动画效果
  • 需要注意 AnimatedVisibility 和 AnimatedContent 仍然处于 @Experimental 状态,stable 之前也许会有所变化。
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值