Compose - 动画

官方页面

一、概念

1.1 层级关系

1.2 动画分类

基于

可组合项/

修饰符

AnimatedVisibility

可见性(显示/隐藏)

Modifier.animateContentSize()

尺寸变化(调整大小)

AnimatedContent

内容根据状态发生改变时

SharedTransitionLayout

共享元素(跳转页面时的同一元素)

基于值

animate***AsState

单个值(透明度)

updateTransition

并发动画

InfiniteTransition

无限循环动画

1.3 EnterTransition、ExitTransition 

EnterTransitionExitTransition 
fadeIn

fadeOut

slideIn

slideOut

slideInHorizontally

slideOutHorizontally

slideInVertically

slideOutVertically

scaleIn

scaleOut

expendIn

shrinkOut

expendHorizontally

shrinkHorizontally

expendVertically

shrinkVertically

二、高级别动画

2.1 简单值动画 animate***AsState

2.1.1 开箱即用的常见数值类型

为单个值添加动画。只需要指定目标值,会从当前值向目标值渐变。

Intfun animateIntAsState(
    targetValue: Int,
    animationSpec: AnimationSpec<Int> = intDefaultSpring,        //动画规格
    label: String = "IntAnimation",
    finishedListener: ((Int) -> Unit)? = null        //动画结束的回调
): State<Int>
Floatfun animateFloatAsState(
    targetValue: Float,
    animationSpec: AnimationSpec<Float> = defaultAnimation,
    visibilityThreshold: Float = 0.01f,
    label: String = "FloatAnimation",
    finishedListener: ((Float) -> Unit)? = null
): State<Float>
Colorfun animateColorAsState(
    targetValue: Color,
    animationSpec: AnimationSpec<Color> = colorDefaultSpring,
    label: String = "ColorAnimation",
    finishedListener: ((Color) -> Unit)? = null
): State<Color>
Dpfun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp>
Sizefun animateSizeAsState(
    targetValue: Size,
    animationSpec: AnimationSpec<Size> = sizeDefaultSpring,
    label: String = "SizeAnimation",
    finishedListener: ((Size) -> Unit)? = null
): State<Size>
Offsetfun animateOffsetAsState(
    targetValue: Offset,
    animationSpec: AnimationSpec<Offset> = offsetDefaultSpring,
    label: String = "OffsetAnimation",
    finishedListener: ((Offset) -> Unit)? = null
): State<Offset>
Rectfun 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(
    targetValue: T,        //目标值
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember { spring() },        //动画规格
    visibilityThreshold: T? = null,
    label: String = "ValueAnimation",
    finishedListener: ((T) -> Unit)? = null        //动画结束的回调
): State<T>

内部实现是会开一个协程去逐渐的改变一个百分比的值,从而达到从一个状态动画过度到另一个状态的效果。

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(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
        (fadeIn(animationSpec = tween(220, delayMillis = 90)) +
            scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
            .togetherWith(fadeOut(animationSpec = tween(90)))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    label: String = "AnimatedContent",
    contentKey: (targetState: S) -> Any? = { it },
    content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
)

使用 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
fun <T> snap(

    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
        )
    )
    // ...
}

3.3 AnimationVector

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值