一、概念
1.1 层级关系
1.2 动画分类
基于 可组合项/ 修饰符 | AnimatedVisibility 可见性(显示/隐藏) | |
Modifier.animateContentSize() 尺寸变化(调整大小) | ||
AnimatedContent 内容根据状态发生改变时 | ||
SharedTransitionLayout 共享元素(跳转页面时的同一元素) | ||
基于值 | animate***AsState 单个值(透明度) | |
updateTransition 并发动画 | ||
InfiniteTransition 无限循环动画 |
1.3 EnterTransition、ExitTransition
EnterTransition | ExitTransition | ||||
fadeIn | fadeOut | ||||
slideIn | slideOut | ||||
slideInHorizontally | slideOutHorizontally | ||||
slideInVertically | slideOutVertically | ||||
scaleIn | scaleOut | ||||
expendIn | shrinkOut | ||||
expendHorizontally | shrinkHorizontally | ||||
expendVertically | shrinkVertically |
二、高级别动画
2.1 简单值动画 animate***AsState
2.1.1 开箱即用的常见数值类型
为单个值添加动画。只需要指定目标值,会从当前值向目标值渐变。
Int | fun animateIntAsState( targetValue: Int, animationSpec: AnimationSpec<Int> = intDefaultSpring, //动画规格 label: String = "IntAnimation", finishedListener: ((Int) -> Unit)? = null //动画结束的回调 ): State<Int> |
Float | fun animateFloatAsState( targetValue: Float, animationSpec: AnimationSpec<Float> = defaultAnimation, visibilityThreshold: Float = 0.01f, label: String = "FloatAnimation", finishedListener: ((Float) -> Unit)? = null ): State<Float> |
Color | fun animateColorAsState( targetValue: Color, animationSpec: AnimationSpec<Color> = colorDefaultSpring, label: String = "ColorAnimation", finishedListener: ((Color) -> Unit)? = null ): State<Color> |
Dp | fun animateDpAsState( targetValue: Dp, animationSpec: AnimationSpec<Dp> = dpDefaultSpring, label: String = "DpAnimation", finishedListener: ((Dp) -> Unit)? = null ): State<Dp> |
Size | fun animateSizeAsState( targetValue: Size, animationSpec: AnimationSpec<Size> = sizeDefaultSpring, label: String = "SizeAnimation", finishedListener: ((Size) -> Unit)? = null ): State<Size> |
Offset | fun animateOffsetAsState( targetValue: Offset, animationSpec: AnimationSpec<Offset> = offsetDefaultSpring, label: String = "OffsetAnimation", finishedListener: ((Offset) -> Unit)? = null ): State<Offset> |
Rect | fun animateRectAsState( targetValue: Rect, animationSpec: AnimationSpec<Rect> = rectDefaultSpring, label: String = "RectAnimation", finishedListener: ((Rect) -> Unit)? = null ): State<Rect> |
IntOffset IntSize | animateIntOffsetAsState animateIntSizeAsState |
val value = flow { //透明度逐渐降低
emit(1.0F)
delay(500)
emit(0.8F)
delay(500)
emit(0.6F)
delay(500)
emit(0.4F)
delay(500)
emit(0.2F)
}
@Composable
fun Show() {
val myAlpah: Float by animateFloatAsState(targetValue = value.collectAsState(initial = 1.0F).value)
Box(
modifier = Modifier
.graphicsLayer(alpha = myAlpah)
.background(Color.Red)
) {
Text(text = "你好")
}
}
2.1.2 为其它数据类型提供支持
animateValueAsState | fun <T, V : AnimationVector> animateValueAsState( 内部实现是会开一个协程去逐渐的改变一个百分比的值,从而达到从一个状态动画过度到另一个状态的效果。 |
TwoWayConverter | 需要自己去实现类型的转换。 |
class CustomSize(val width: Dp, val height: Dp)
@Composable
private fun Content() {
var change by remember { mutableStateOf(true) }
val sizeState: CustomSize by animateValueAsState<CustomSize, AnimationVector2D>(
targetValue = if (change) CustomSize(200.dp, 200.dp) else CustomSize(80.dp, 80.dp),
typeConverter = TwoWayConverter(
convertFromVector = { CustomSize(it.v1.dp, it.v2.dp) },
convertToVector = { AnimationVector2D(it.width.value, it.height.value) }
),
label = ""
)
Column(Modifier.size(250.dp)) {
Box(modifier = Modifier
.size(sizeState.width, sizeState.height)
.clickable { change = !change }
.background(Color.Red)
)
}
}
2.2 可见性动画 AnimatedVisibility()
为内容的显示或消失添加动画。默认以“淡入扩大出现,淡出缩小消失”,可通过 EnterTransition 和 ExitTransition 设置,不同的动画可以用“+”自由组合。
@Composable fun AnimatedVisibility( visible: Boolean, //控制内容是否可见 modifier: Modifier = Modifier, enter: EnterTransition = fadeIn() + expandIn(), //进入动画(默认淡入扩大) exit: ExitTransition = shrinkOut() + fadeOut(), //退出动画(默认淡出缩小) label: String = "AnimatedVisibility", content: @Composable() AnimatedVisibilityScope.() -> Unit //显示或消失的内容 ) |
val value = flow { //可见不可见切换
emit(true)
delay(500)
emit(false)
delay(500)
emit(true)
delay(500)
emit(false)
}
@Composable
fun Show() {
val enable: Boolean by value.collectAsState(initial = true)
//包裹住需要控制的内容(这里是Text)
AnimatedVisibility(
visible = enable,
){
Text(text = "你好")
}
}
2.2.1 监听动画状态 MutableTransitionState
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"
}
)
}
2.2.2 为直接或间接子元素单独设置动画效果 animateEnterExit
可以为 AnimatedVisibility 不应用动画,而为子元素们单独设置。
AnimatedVisibility(
visible = visible,
enter = EnterTransition.None, //可以不应用动画
exit = ExitTransition.None
) {
// Fade in/out the background and the foreground.
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
//为子元素单独设置
enter = slideInVertically(),
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)
.background(Color.Red)
) { }
}
}
2.3 内容改变动画 AnimatedContent
内容根据状态发生改变时添加动画。默认淡出后淡入。
fun <S> AnimatedContent( 使用 transitionSpec 指定 ContentTransform 对象来配置进入动画和退出动画(togetherWith 中缀表达式,是进入动画的扩展函数,传入退出动画,返回ContentTransform对象)。 |
Column {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("添加数据")
}
AnimatedContent(
targetState = count,
transitionSpec = {
//togetherWith 中缀表达式,是进入动画的扩展函数,传入退出动画,返回的是ContentTransform
scaleIn() togetherWith scaleOut()
}
) { targetCount ->
Text(text = "数值:${targetCount}") //使用targetCount而不是count
}
}
2.4 尺寸改变动画 Modifier.animateContentSize( )
为可大小变化的控件添加动画(如展开收起)。注意:在修饰符链中的顺序很重要,为了确保动画流畅,务必放置在任何大小修饰符(如size或defaultMinSize)前面,以确保会将带动画效果的值的变化报告给布局。
Modifier.animateContentSize( animationSpec: FiniteAnimationSpec<IntSize> = spring(), //动画规格 finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null //动画结束的回调 ) |
@Composable
private fun Content() {
var isExpanded by remember{ mutableStateOf(false) }
Column(Modifier.size(200.dp)) {
Button(modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = { isExpanded = !isExpanded }
) {
Text(text = if (isExpanded) "展开" else "收起")
}
Text(
modifier = Modifier
.animateContentSize(
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
.fillMaxWidth(),
text = "Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World! Hello World!",
maxLines = if (isExpanded) 5 else 1
)
}
}
2.5 视图切换动画 Crossfade
为两个内容的切换添加淡入淡出动画。
fun <T> Crossfade( targetState: T, //目标状态,每次更改都会触发动画 modifier: Modifier = Modifier, animationSpec: FiniteAnimationSpec<Float> = tween(), //动画规格 label: String = "Crossfade", content: @Composable (T) -> Unit ) |
@Composable
fun Sample() {
var currentPage by remember { mutableStateOf(false) }
Column {
Crossfade(targetState = currentPage, animationSpec = tween(3000), label = "") { screen ->
when (screen) {
false -> Box(modifier = Modifier.background(Color.Blue).size(100.dp))
true -> Box(modifier = Modifier.background(Color.Red).size(100.dp))
}
}
Button(onClick = { currentPage = !currentPage }, modifier = Modifier.width(100.dp)) {
Text(text = "点击切换")
}
}
}
2.6 多值动画 updateTransiton
统一管理多个动画。定义一个状态,用 updateTransition() 包裹创建 Transition 实例,然后去调用不同的动画方法(有简单值动画 transition.animateXXX()、可见性动画 transition.AnimatedVisibility()、内容改变动画 transition.AnimatedContent())为每个状态指定不同效果。当状态改变时 Transition 实例能监听并执行对应的动画。
@Composable fun <T> updateTransition( targetState: T, //目标状态,可以是任何数据类型,建议enum确保类型安全 label: String? = null ): Transition<T> |
enum class DemoState { Default, Clicked }
@Composable
private fun Content() {
var demoState by remember { mutableStateOf(DemoState.Default) }
val transition = updateTransition(demoState, label = "")
//边框宽度动画
val elevation by transition.animateDp(label = "") { state ->
if (state == DemoState.Default) 5.dp else 15.dp
}
//背景色动画 + 指定AnimationSpec
val borderColor by transition.animateColor(
transitionSpec = {
when {
DemoState.Default isTransitioningTo DemoState.Clicked ->
spring(stiffness = 50f)
else ->
tween(durationMillis = 500)
}
}, label = ""
) { state ->
if (state == DemoState.Default) Color.Blue else Color.Red
}
Surface(
onClick = {
demoState = if (transition.currentState == DemoState.Default) DemoState.Clicked else DemoState.Default
},
border = BorderStroke(elevation, Color.Green),
color = borderColor
) {
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text(text = "点击切换")
//内容改变动画AnimatedContent
transition.AnimatedContent { demoState ->
if (demoState == DemoState.Default) {
Text(text = "内容改变动画AnimatedContent(改变前前前)")
} else {
Text(text = "内容改变动画AnimatedContent(改变后后后)")
}
}
//可见性动画AnimatedVisibility
transition.AnimatedVisibility(
visible = { state ->
state != DemoState.Default
},
enter = expandVertically(),
exit = shrinkVertically()
) {
Text(text = "可见性动画AnimatedVisibility")
}
}
}
}
2.6.1 封装以便复用
在处理具有大量动画值的复杂组件时,将动画和界面分开会更方便复用。
enum class BoxState { Collapsed, Expanded }
@Composable
fun AnimatingBox(boxState: BoxState) {
val transitionData = updateTransitionData(boxState)
// UI tree
Box(
modifier = Modifier
.background(transitionData.color)
.size(transitionData.size)
)
}
// Holds the animation values.
private class TransitionData(
color: State<Color>,
size: State<Dp>
) {
val color by color
val size by size
}
// Create a Transition and return its animation values.
@Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState)
val color = transition.animateColor { state ->
when (state) {
BoxState.Collapsed -> Color.Gray
BoxState.Expanded -> Color.Red
}
}
val size = transition.animateDp { state ->
when (state) {
BoxState.Collapsed -> 64.dp
BoxState.Expanded -> 128.dp
}
}
return remember(transition) { TransitionData(color, size) }
}
2.6.2 无限循环 rememberInfiniteTransition
可以像 Transition 一样保存一个或多个子动画,但是这些动画一进入组合阶段就开始运行,除非被移除否则不会停止。
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))
2.7 无限循环动画 rememberInfiniteTransition
动画一进入组合阶段就开始运行,除非被移除,否则不会停止。使用 rememberInfiniteTransition 创建 InfiniteTransition 实例,使用 animateColor、animatedFloat 或 animatedValue 添加子动画,还需要指定 infiniteRepeatable 以指定动画规范。
@Composable
fun Show() {
val infiniteTransition = rememberInfiniteTransition()
val alpha = infiniteTransition.animateFloat(
initialValue = 0F, //初始值
targetValue = 1F, //目标值
animationSpec = InfiniteRepeatableSpec(
animation = keyframes {
durationMillis = 1000
1F at 500 //指定关键帧
},
repeatMode = RepeatMode.Restart //重复模式,还有一个Reverse
)
)
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
}
二、低级别动画
2.1 Animatable
2.2 Animation
三、自定义动画
3.1 动画规范 AnimationSpec
在 View 界面系统中,对于基于时长的动画需要使用 ObjectAnimator 等API,对于基于物理特性的动画需要使用 SpringAnimation。同时使用这两个不同的动画 API 并不容易,Compose 中的 AnimationSpec 够以统一的方式处理这些动画。
对于动画的速度曲线的限制,传统属性动画用的是插值器 Interpolator,而在 Compose 中用的是 AnimationSpec。
spring | 可在起始值和结束值之间创建基于物理特性的动画。相比基于时长的 AnimationSpec 类型,spring 可以更流畅地处理中断,因为它可以在目标值在动画中变化时保证速度的连续性。spring 用作很多动画 API(如 animate*AsState 和 updateTransition)的默认 AnimationSpec。 |
tween | 使用缓和曲线在起始值和结束值之间添加动画效果。 |
keyframes | 会根据在动画时长内的不同时间戳中指定的快照值添加动画效果。在任何给定时间,动画值都将插值到两个关键帧值之间。对于其中每个关键帧,可以指定 Easing 来确定插值曲线。可以选择在 0 毫秒和持续时间处指定值。如果不指定这些值,它们将分别默认为动画的起始值和结束值。 |
repeatable | 反复运行基于时长的动画(例如 tween 或 keyframes)直至达到指定的迭代计数。 |
infiniteRepeatable | 重复无限次的迭代。 |
snap | 立即将值切换到结束值。 |
3.1.1 spring()
@Stable fun <T> spring( dampingRatio: Float = Spring.DampingRatioNoBouncy, //弹簧的弹性 stiffness: Float = Spring.StiffnessMedium, //弹簧应向结束值移动的速度 visibilityThreshold: T? = null ): SpringSpec<T> = SpringSpec(dampingRatio, stiffness, visibilityThreshold) |
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
)
)
3.1.2 tween()
@Stable fun <T> tween( durationMillis: Int = DefaultDurationMillis, //动画的持续时间 delayMillis: Int = 0, //延迟动画开始的时间 easing: Easing = FastOutSlowInEasing ): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing) |
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 50,
easing = LinearOutSlowInEasing
)
)
3.1.3 keyframes
@Stable fun <T> keyframes( init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit ): KeyframesSpec<T> { return KeyframesSpec(KeyframesSpec.KeyframesSpecConfig<T>().apply(init)) } |
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = keyframes {
durationMillis = 375
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
0.4f at 75 // ms
0.4f at 225 // ms
}
)
3.1.4 repeatable
@Stable fun <T> repeatable( iterations: Int, animation: DurationBasedAnimationSpec<T>, repeatMode: RepeatMode = RepeatMode.Restart, //从头开始还是从尾开始 initialStartOffset: StartOffset = StartOffset(0) ): RepeatableSpec<T> = RepeatableSpec(iterations, animation, repeatMode, initialStartOffset) |
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = repeatable(
iterations = 3,
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
3.1.5 infiniteRepeatable
@Stable fun <T> infiniteRepeatable( animation: DurationBasedAnimationSpec<T>, repeatMode: RepeatMode = RepeatMode.Restart, //从头开始还是从尾开始 initialStartOffset: StartOffset = StartOffset(0) ): InfiniteRepeatableSpec<T> = InfiniteRepeatableSpec(animation, repeatMode, initialStartOffset) |
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 300),
repeatMode = RepeatMode.Reverse
)
)
3.1.6 snap
@Stable delayMillis: Int = 0 //延迟动画开始的时间 ) = SnapSpec<T>(delayMillis) |
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = snap(delayMillis = 50)
)
3.2 Easing
基于时长的 AnimationSpec 操作(如 tween 或 keyframes)使用 Easing 来调整动画的小数值。这样可让动画值加速和减速,而不是以恒定的速率移动。Easing 实际上是一个函数,它取一个介于 0 和 1.0 之间的小数值并返回一个浮点数。返回的值可能位于边界之外,表示过冲或下冲。
- Easing 对象的运行方式与平台中 Interpolator 类的实例相同。不过,它采用的不是 getInterpolation() 方法,而是 transform() 方法。
- FastOutSlowInEasing、LinearOutSlowInEasing、FastOutLinearEasing、LinearEasing、CubicBezierEasing 还有更多。
val CustomEasing = Easing { fraction -> fraction * fraction }
@Composable
fun EasingUsage() {
val value by animateFloatAsState(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = CustomEasing
)
)
// ...
}