本篇进入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 有四种类型:
- fade:
fadeIn - scale:
scaleIn - slide:
slideIn , slideInHorizontally , slideInVertically - expand:
expandIn , expandHorizontally , expandVertically
exit :消失动画,默认是收缩(与显示相反)时淡出。ExitTransition 如下:
- fade:
fadeOut - scale:
scaleOut - slide:
slideOut , slideOutHorizontally , slideOutVertically - shrink:
shrinkOut , shrinkHorizontally ,shrinkVertically
多个动画效果我们可以使用 + 运算符进行组合,使用起来很方便。具体这些动画的效果我们可以参考官方文档的示例动图:EnterTransition、ExitTransition 示例
需要注意的是,Row 和Column 下的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 中的显示隐藏动画是expandHorizontally 和shrinkHorizontally ,所以是水平方向从左到右展开时淡入,从右到左收缩时淡出。Column 就是垂直方向过渡。这点我们在使用时需要注意。
AnimatedVisibility 还提供了传入 MutableTransitionState 的变体方法。该属性有助于观察动画状态。官方demo:
val state = remember {
MutableTransitionState(false).apply {
targetState = true
}
}
Column {
AnimatedVisibility(visibleState = state) {
Text(text = "Hello, world!")
}
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.None 和 ExitTransition.None 。
添加自定义动画
通过AnimatedVisibility 的transition 属性访问底层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 中的initialState 和targetState 等属性,来自定义动画效果。
下面看一个数字变换的例子:
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
animateContentSize 是Modifier 的一个扩展方法,可以在内容大小发生变化时添加动画过渡效果。此方法使用简单,这里就不过多的说明了。
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 同时也有AnimatedVisibility 与 AnimatedContent 的拓展方法。)
具体实现的效果就是展开时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 与自定义动画部分。
参考
|