AnimatedVisibility

对单个控件做显示和隐藏的动画效果

在上一个章节我们学习了 Transition 动画,这一节学习它更加上层的 Api,但其实内部本质上使用的也是 Transition, 先看一下 AnimatedVisibility 的源码, 先看看里面的一些参数配置, 了解了一些参数配置, 就很简单了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Composable
fun ColumnScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandVertically(),
    exit: ExitTransition = fadeOut() + shrinkVertically(),
    label: String = "AnimatedVisibility",
    content: @Composable AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visible, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

简单分析一下这几个方法参数, 基本上就知道怎么使用了,

  • 第一个参数是 visible 是控制是否显示
  • 第二个参数是 modifier 修饰符
  • 第三个参数是配置的入场动画, 默认参数是 fadeIn() + expandVertically(), 是一个淡入 + 垂直方向展开的一个动画
  • 第四个参数是退场动画, 默认参数是 fadeOut() + shrinkVertically() 是一个淡出 + 垂直方向收缩动画
  • 第五个参数可以当做是起的一个名称
  • 最后一个参数的类型是一个带有 @Composable 注解的函数类型参数, 是我们编写组件的代码

在入场动画和出场动画配置中, 我们发现他的返回值是 EnterTransition 和 ExitTransition, 可以看看 fadeIn 和 fadeOut,淡入淡出的动画

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fun fadeIn(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    initialAlpha: Float = 0f
): EnterTransition {
    return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}

fun fadeOut(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    targetAlpha: Float = 0f,
): ExitTransition {
    return ExitTransitionImpl(TransitionData(fade = Fade(targetAlpha, animationSpec)))
}

这里可以发现他都是调用 EnterTransitionImpl 构造,并传入了 TransitionData 对象, 其实对动画渐变效果设置都在这个类里面了

1
2
3
4
5
6
internal data class TransitionData(
    val fade: Fade? = null, // 淡入淡出动画
    val slide: Slide? = null, // 滑动
    val changeSize: ChangeSize? = null, // 大小的裁切 (会改变画面大小的动画都是这个参数,比如 shrinkOut)
    val scale: Scale? = null // 缩放动画
)

所以, 我们不管配置的入场还是出场动画效果, 最终都会保存在这个类中

还有一个问题就是我们看到配置动画的方式是用 “ + ” 加号来完成的, 那么他是如何做到的呢, 其实他使用的也是中缀函数, 上几个篇章我们已经看到它很多身影了, 源码是这样的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  operator fun plus(enter: EnterTransition): EnterTransition {
        return EnterTransitionImpl(
            TransitionData(
                fade = data.fade ?: enter.data.fade, 
                slide = data.slide ?: enter.data.slide, 
                changeSize = data.changeSize ?: enter.data.changeSize,
                scale = data.scale ?: enter.data.scale
            )
        )
    }

如果以默认参数 fadeIn() + expandVertically() 调用的来看的话, 最终赋值的参数就是 fade 和 changeSize , 其实就是配置 TransitionData 的 4 个值, 但是如果相同的动画的画,比如 fade + fade 那么会被覆盖掉

最后可以看到内部也是调用的 updateTransition 方法, 这个上节我们已经学过了, 最后一个小例子看看效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fun TransitionPro() {
    var show by remember { mutableStateOf(true) }
    
    Column(modifier = Modifier.fillMaxSize()) {
         AnimatedVisibility( visible = show ) {
            Box(
                modifier = Modifier
                    .size(200.dp)
                    .background(Color.Red)
            )
        }
                Button(modifier = Modifier.alpha(0.3f), onClick = {
            show = !show
        }) {
            Text(text = "测试")
        }
    }
}

Transition 入场和出场动画

  • fadeln:淡入动画。该动画会使对象逐渐从不可见到可见,产生渐变效果
  • fadeOut:淡出动画。该动画会使对象逐渐从可见到不可见,产生渐变效果。
  • slideOut:滑出动画。该动画会使对象在给定的方向上滑动离开屏幕。
  • scaleln:缩放进入动画。该动画会使对象逐渐从较小的大小缩放到正常大小。
  • scaleOut:缩放离开动画。该动画会使对象逐渐从正常大小缩放到较小的大小。
  • expandln:展开动画。该动画会使对象从一个较小的尺寸逐渐展开到一个较大的尺寸。
  • shrinkOut:收缩动画。该动画会使对象从一个较大的尺寸逐渐收缩到一个较小的尺寸。
  • expandHorizontally:水平展开动画。该动画会使对象从一个较窄的宽度逐渐展开到一个较宽的宽度。
  • expandVertically:垂直展开动画。该动画会使对象从一个较短的高度逐渐展开到一个较高的高度。
  • shrinkHorizontally:水平收缩动画。该动画会使对象从一个较短的宽度逐渐收缩到一个较窄的宽度。
  • shrinkVertically:垂直收缩动画。该动画会使对象从一个较高的高度逐渐收缩到一个较短的高度。
  • slidelnHorizontally:水平滑入动画。该动画会使对象从给定方向的屏幕外滑入,直到达到正常位置。
  • slidelnVertically:垂直滑入动画。该动画会使对象从给定方向的屏幕外滑入,直到达到正常位置。
  • slideOutHorizontally:水平滑出动画。该动画会使对象从正常位置滑出至给定方向的屏幕外。
  • slideOutVertically:垂直滑出动画。该动画会使对象从正常位置滑出至给定方向的屏幕外。

在 AnimatedVisibility 的入场和出场动画中, 一共可以配置这些参数, 下面我们一个一个场景分析, 介绍一些区别

fadeIn

1
2
3
4
5
6
fun fadeIn(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    initialAlpha: Float = 0f  // 初始的透明度
): EnterTransition {
    return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}

slideIn

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Stable
fun slideIn(
    animationSpec: FiniteAnimationSpec<IntOffset> =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = IntOffset.VisibilityThreshold
        ),
    initialOffset: (fullSize: IntSize) -> IntOffset, // 初始的偏移值, 其中 fullSize 是整个控件的大小
): EnterTransition {
    return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
}

fadeOut

1
2
3
4
5
6
7
// 基本和 fadeIn 一致, 唯一 targetAlpha 不同, targetAlpha 表示的是目标状态的时候透明度
fun fadeOut(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    targetAlpha: Float = 0f,
): ExitTransition {
    return ExitTransitionImpl(TransitionData(fade = Fade(targetAlpha, animationSpec)))
}

scaleOut

1
2
3
4
5
6
7
8
9
fun scaleOut(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    targetScale: Float = 0f, // 目标状态的缩放值
    transformOrigin: TransformOrigin = TransformOrigin.Center // 从中心点缩放
): ExitTransition {
    return ExitTransitionImpl(
        TransitionData(scale = Scale(targetScale, transformOrigin, animationSpec))
    )
}

场景一

其中一些参数说明都写在注释上了, 至于 animationSpec 的配置, 不再说明, 在之前的文章都有描述, 最终我们使用一些上面的几个动画, 看看效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun TransitionPro() {
    var show by remember { mutableStateOf(true) }
    
    Column(modifier = Modifier.fillMaxSize()) {
                 AnimatedVisibility(
            visible = show, enter = fadeIn(animationSpec = tween(durationMillis = 2000)) + slideIn(animationSpec = tween(durationMillis = 2000)) {fullsize->
                IntOffset(fullsize.width / 2, 0)
            }, exit = fadeOut(animationSpec = tween(4000)) + scaleOut(animationSpec = tween(durationMillis = 2000),targetScale = 0.5f)
        )  { 
            Box(
                modifier = Modifier
                    .size(200.dp)
                    .background(Color.Red)
            )
        }
                Button(modifier = Modifier.alpha(0.3f), onClick = {
            show = !show
        }) {
            Text(text = "测试")
        }
    }
}

这段代码的意思是

  • 入场动画是一个淡入的动画, 并且配置的是 tween spec 设置的时间为 5000 毫秒, 加一个从 x 轴方向平移, fullsize.width / 2, 也就是控件的一半, 以初始状态控件的一半从右往左边滑入目标状态的过程
  • 出场动画是淡出的动画, 并且配置的是 tween spec 设置的时间为 4000 毫秒, 加一个缩放动画, 配置的目标缩放状态是 0.5f, 也就是控件的一半

最终运行代码后, 效果如下

shrinkOut

shrinkOut 是一个收缩动画,会改变控件大小,它对应 TransitionData 中的属性是 changeSize , 首先看看 shrinkOut 的源码, 他的一些参数配置, 后面再看怎么使用的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun shrinkOut(
    animationSpec: FiniteAnimationSpec<IntSize> =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = IntSize.VisibilityThreshold
        ),
    shrinkTowards: Alignment = Alignment.BottomEnd,
    clip: Boolean = true,
    targetSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
): ExitTransition {
    return ExitTransitionImpl(
        TransitionData(
            changeSize = ChangeSize(shrinkTowards, targetSize, animationSpec, clip)
        )
    )
}

shrinkOut 中有不同的地方是 shrinkTowards 和 clip

  • clip : 是否剪裁动画边界外的内容,默认为 true 会对控件进行裁切,false 不会对控件进行裁切, 只做动画的位移效果
  • shrinkTowards :表示收缩的目标方向,也就是当前位置往哪个方向收缩, 和 expand 相反的作用 默认 Alignment.BottomEnd, 也就是右下角

场景二

这里我们先设置 clip 都为 true 的情况, 也就是默认情况下的效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@Preview
@Composable
fun ShrinkExample() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        var isVisibleCenter by remember { mutableStateOf(true) }
        var isVisibleBottomEnd by remember { mutableStateOf(true) }
        var isVisibleTopStart by remember { mutableStateOf(true) }

        AnimatedVisibility(
            visible = isVisibleCenter,
            enter = expandIn(),
            exit = shrinkOut(animationSpec = tween(3000),shrinkTowards = Alignment.Center, clip = true )
        ) {
            Card(
                modifier = Modifier
                    .size(150.dp)
                    .padding(16.dp)
                    .clickable { isVisibleCenter = !isVisibleCenter }
            ) {
                Text(
                    text = "Center with clip",
                    modifier = Modifier.align(Alignment.Start),
                    textAlign = TextAlign.Center
                )
            }
        }

        Spacer(modifier = Modifier.height(16.dp))

        AnimatedVisibility(
            visible = isVisibleBottomEnd,
            enter = expandIn(),
            exit = shrinkOut(animationSpec = tween(3000), shrinkTowards = Alignment.BottomEnd,clip = true)
        ) {
            Card(
                modifier = Modifier
                    .size(150.dp)
                    .padding(16.dp)
                    .clickable { isVisibleBottomEnd = !isVisibleBottomEnd }
            ) {
                Text(
                    text = "Bottom End(右下角)",
                    modifier = Modifier.align(Alignment.Start),
                    textAlign = TextAlign.Center
                )
            }
        }

        Spacer(modifier = Modifier.height(16.dp))

        AnimatedVisibility(
             visible = isVisibleTopStart,
            enter = expandIn(),
            exit = shrinkOut(animationSpec = tween(3000), shrinkTowards = Alignment.TopStart,clip = true)
        ) {
            Card(
                modifier = Modifier
                    .size(150.dp)
                    .padding(16.dp)
                    .clickable { isVisibleTopStart = !isVisibleTopStart }
            ) {
                Text(
                    text = "Top Start(左上角)",
                    modifier = Modifier.align(Alignment.Start),
                    textAlign = TextAlign.Center
                )
            }
        }
    }
}

代码运行后如下

这是分别设置为中心点, 右下角和左上角的收缩的目标方向, 并且是裁切画面的效果, 接下来, 这3种动画其余的代码都不变化, 只是修改一个参数 clip 为 false 再看看效果

可以看得出, 唯一的区别是, 收缩的时候, 对画面没有进行裁切, 在动画结束后,才隐藏

slide

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fun slideIn(
    animationSpec: FiniteAnimationSpec<IntOffset> =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = IntOffset.VisibilityThreshold
        ),
    initialOffset: (fullSize: IntSize) -> IntOffset, // 初始状态
): EnterTransition {
    return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
}

fun slideOut(
    animationSpec: FiniteAnimationSpec<IntOffset> =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = IntOffset.VisibilityThreshold
        ),
    targetOffset: (fullSize: IntSize) -> IntOffset, // 目标终点状态
): ExitTransition {
    return ExitTransitionImpl(TransitionData(slide = Slide(targetOffset, animationSpec)))
}

slideIn 是一个滑入动画, slideOut 是一个滑出动画,设置的参数都不多,一个是动画的 animationSpec,还是一个就是滑入或者滑出的起点或者终点位置,接下来看看这个例子

场景三

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        AnimatedVisibility(visible = show, enter = slideIn(animationSpec = tween(durationMillis = 1500)) {
            /** 从初始状态 --it.width, -it.height 到 fullSize 的宽度 **/
            IntOffset(-it.width, -it.height)
        }, exit = fadeOut(animationSpec = tween(durationMillis = 1500)) + shrinkOut(
            animationSpec = tween(durationMillis = 1500), shrinkTowards = Alignment.CenterStart, clip = false
        ) {
            /** 从初始状态切换到目标状态, 也就是 fullzise 切换到 0 的大小 **/
            IntSize(0, 0)
        }) {
            Box(
                modifier = Modifier
                    .size(200.dp)
                    .clip(RoundedCornerShape(40.dp))
                    .background(Color.Green)
            )
        }

上面的代码就是动画出现的时候是以 -it.width, -it.height 的起始坐标点,也就是左上角屏幕外的位置,然后以 tween 1500 毫秒的时间滑入到 fullSize 的位置,控件隐藏的动画是淡出的效果加收缩动画,并且设置了 clip 为不裁切画面的方式,运行后效果如下

expandIn

expandIn 是一个展开动画,和 shrinkOut 刚好相反,其中他也是个会改变控件大小的动画,参数基本也和 shrinkOut 一样,唯一不同的是 expandFrom ,表示的是从哪里展开,也就是起始开始位置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fun expandIn(
    animationSpec: FiniteAnimationSpec<IntSize> =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = IntSize.VisibilityThreshold
        ),
    expandFrom: Alignment = Alignment.BottomEnd,
    clip: Boolean = true,
    initialSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
): EnterTransition {
    return EnterTransitionImpl(
        TransitionData(
            changeSize = ChangeSize(expandFrom, initialSize, animationSpec, clip)
        )
    )
}

场景四

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
AnimatedVisibility(visible = show, exit = shrinkOut(
    animationSpec = tween(durationMillis = 5000),
    shrinkTowards = Alignment.BottomEnd, clip = true
), enter = expandIn(
    animationSpec = tween(durationMillis = 5000), expandFrom = Alignment.TopStart, clip = true
) {
    IntSize(0, 0)
}) {
   Box(
    modifier = Modifier
        .size(156.dp)
        .clip(RoundedCornerShape(40.dp))
        .background(Color.Red)
	)	
}

这段代码配置的是入场是一个展开动画,且是从 TopStart 左上角扩大的,退场动画是从 BottomEnd 右下角收缩的一个动画,运行后效果如下

scaleIn scaleOut

scaleIn 和 scaleOut 是一个缩放动画,scaleIn 是代表入场,scaleOut 代表的是退场,需要注意的是,他和 expandIn 和 shrinkOut 不一样,这两个会改变控件的大小,会对控件进行裁切,而 scaleIn scaleOut 不会,scaleIn scaleOut 只是对控件本身进行大小缩放

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fun scaleIn(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    initialScale: Float = 0f, // 初始状态缩放大小
    transformOrigin: TransformOrigin = TransformOrigin.Center, // 缩放的相对位置 (Center 代表的是中心点)
): EnterTransition {
    return EnterTransitionImpl(
        TransitionData(scale = Scale(initialScale, transformOrigin, animationSpec))
    )
}

fun scaleOut(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    targetScale: Float = 0f, // 目标状态缩放大小
    transformOrigin: TransformOrigin = TransformOrigin.Center // 缩放的相对位置 (Center 代表的是中心点)
): ExitTransition {
    return ExitTransitionImpl(
        TransitionData(scale = Scale(targetScale, transformOrigin, animationSpec))
    )
}

这里的参数区别就是 initialScale 和 targetScale 配置的区别了,然后他们都是一个 scale 动画,配置在 TransitionData 对象中的 scale 属性中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
AnimatedVisibility(
    visible = show, exit = scaleOut(animationSpec = tween(durationMillis = 1000)), enter = scaleIn(
        animationSpec = tween(4000), initialScale = 0.3f, transformOrigin = TransformOrigin(0f, 0f)
    )
) {
        Box(
        modifier = Modifier
            .size(156.dp)
            .clip(RoundedCornerShape(40.dp))
            .background(Color.Red)
    )
}

这段代码配置的入场动画是初始缩放大小为 0.3f,缩放的相对位置为 0f,0f 也就是左上角的位置,退场动画为 tween 为1000 毫秒,而相对位置默认参数的话,就是中间位置,目前状态缩放比例是 0f, 也就是完全消失,最后运行代码看看效果

AnimatedVisibility 总结

好了,AnimatedVisibility 的使用和配置基本上都讲完了,本质上它是对单个控件的显示和隐藏做渐变效果,也就是入场和出场的动画,而入场和出场的动画都是对 TransitionData 中的 fade slide changeSize scale 这4个属性做配置,他们的对应关系是这样的

  • fade 淡入淡出效果
    • fadeln 淡入动画,该动画会使对象逐渐从不可见到可见,产生渐变效果
    • fadeOut 淡出动画,该动画会使对象逐渐从可见到不可见,产生渐变效果
  • slide 滑入滑出效果
    • slideOut 滑出动画
    • slideIn 滑入动画
    • slidelnHorizontally 水平滑入动画
    • slidelnVertically 垂直滑入动画
    • slideOutHorizontally 水平滑出动画
    • slideOutVertically 垂直滑出动画
  • changeSize 收缩动画效果 (会对控件大小进行裁切)
    • expandIn 展开动画
    • shrinkOut 收缩动画
    • expandHorizontally 水平展开动画
    • expandVertically 垂直展开动画
    • shrinkHorizontally 水平收缩动画
    • shrinkVertically 垂直收缩动画
  • scale 缩放动画效果
    • scaleln:缩放进入动画
    • scaleOut 缩放退场动画

好了,基本上 AnimatedVisibility 能使用的动画就是这些,在本文中对于 xxxVertically 和 xxxHorizontally 并没有讲,是因为他们本质上就是再上一层的封装,比如 slideIn 动画,而对于 slidelnVertically 只是针对一个方向上的,而 slideIn 可以完全自由配置你想要的坐标偏移,而你学会了 slideIn 的动画,对于单个方向的自然也就会了

CrossFade

对于 AnimatedVisibility 动画而言,他是作用于单个控件的显示和隐藏做渐变动画的,而 CrossFade 就是两个控件,一个消失另一个显示出来,动画效果是淡入淡出,并对尺寸进行处理 (瞬间改变)。Crossfade 只支持这一种动画效果,是对于 AnimatedContent 的简化版本,AnimatedContent 稍后进行介绍

照例看看 Crossfade 源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Composable
fun <T> Crossfade(
    targetState: T, // 初始状态
    modifier: Modifier = Modifier,
    animationSpec: FiniteAnimationSpec<Float> = tween(), 
    label: String = "Crossfade",
    content: @Composable (T) -> Unit
) {
    val transition = updateTransition(targetState, label)
    transition.Crossfade(modifier, animationSpec, content = content)
}

Crossfade 的源码也比较简单,animationSpec 的配置只能是 FiniteAnimationSpec 的子类,也就是 TweenSpec, SpringSpec, KeyframesSpec, RepeatableSpec, SnapSpec 这些,如果你再往 Crossfade 里面看的话,可以看到他对 alpha 做渐变效果而已,也就是看到的淡入淡出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fun test22() {
    Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
        var showPicture by remember { mutableStateOf(false) }
        Crossfade(targetState = showPicture, label = "") {
            if (it) {
                Image(
                    contentScale = ContentScale.Crop,
                    painter = painterResource(id = R.mipmap.aaa),
                    modifier = Modifier.width(300.dp),
                    contentDescription = null
                )
            } else {
                Box(
                    Modifier
                        .height(300.dp * 9 / 16)
                        .width(300.dp)
                        .background(Color.Blue)
                )
            }
        }
        Spacer(modifier = Modifier.height(10.dp))
        Button(onClick = { showPicture = !showPicture }) {
            Text(text = "切换")
        }
    }
}

代码也很简单,使用 showPicture 控制图片的显示和隐藏,运行效果如下

CrossFade 尺寸不一样

如果切换的两个组件,尺寸不一致,就会出现如下情况

如果转场前是大尺寸,转场后是小尺寸,会等转场快结束的时候,变成小尺寸 如果转场前是小尺寸,转场后是大尺寸,会在转场刚开始的时候,变成大尺寸

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Preview
@Composable
fun test222() {
    Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
        var showPicture by remember { mutableStateOf(false) }
        Crossfade(targetState = showPicture, label = "", animationSpec = tween(2000)) {
            if (it) {
                Image(
                    contentScale = ContentScale.Crop,
                    painter = painterResource(id = R.mipmap.aaa),
                    modifier = Modifier.width(100.dp),
                    contentDescription = null
                )
            } else {
                Box(
                    Modifier
                        .height(300.dp * 9 / 16)
                        .width(300.dp)
                        .background(Color.Blue)
                )
            }
        }
        Spacer(modifier = Modifier.height(10.dp))
        Button(onClick = { showPicture = !showPicture }) {
            Text(text = "切换")
        }
    }
}

AnimatedContent

上面说到 crossFade 是作用于两个控件的,而 AnimatedContent 用来控制多个组件的入场和出场,同时还能对入场和出场效果做定制,AnimatedContent 出入场动画效果的尺寸是渐变的,这是区别与 crossFade 的一点,AnimatedContent 的源码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Composable
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 了,transitionSpec 是一个高阶函数,他可以配置入场和出场的动画,同时提供了一个 AnimatedContentTransitionScope 的上下文环境,返回的对象是一个 ContentTransform 对象,其中 AnimatedContentTransitionScope 内部有一个 using 方法,可以配置大小的改变,而 ContentTransform 是用来配置入场和出场的动画的

1
2
3
4
5
6
7
8
9
class ContentTransform(
    val targetContentEnter: EnterTransition, // 入场动画
    val initialContentExit: ExitTransition, // 退场动画
    targetContentZIndex: Float = 0f, // 配置轴, 控件上下的顺序
    sizeTransform: SizeTransform? = SizeTransform() //  控制大小
)

// togetherWith 中缀函数,用来配置退场动画
infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit) 

而上面的默认参数的意思就是入场:淡入 + 缩放动画,退场使用 togetherWith 配置了一个淡出的退场动画,togetherWith 是一个中缀函数,它其实就是配置了 initialContentExit 属性,基本参数配置就是这些,回到之前的例子看看效果

大小尺寸的变化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Composable
fun test232() {
    Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
        var showPicture by remember { mutableStateOf(false) }
        AnimatedContent(showPicture) {
            if (it) {
                Image(
                    contentScale = ContentScale.Crop,
                    painter = painterResource(id = R.mipmap.aaa),
                    modifier = Modifier.width(100.dp),
                    contentDescription = null
                )
            } else {
                Box(
                    Modifier.height(300.dp * 9 / 16).width(300.dp).background(Color.Blue)
                )
            }
        }
        Spacer(modifier = Modifier.height(10.dp))
        Button(onClick = { showPicture = !showPicture }) {
            Text(text = "切换")
        }
    }
}

入场和出场

配置入场和退场的动画效果,模拟 activity 的转场动画

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
fun AnimContent() {
    var show by remember { mutableStateOf(true) }

    Column(modifier = Modifier.fillMaxSize()) {
        AnimatedContent(
            targetState = show, transitionSpec = {
                /** 配置一个 activity 的转场动画 **/
                slideIn(
                    animationSpec = tween(durationMillis = 1000)
                ) {
                    /** 配置初始状态值 **/
                    IntOffset(it.width, 0)
                } togetherWith slideOut(animationSpec = tween(1000)) {
                    /** 目标状态,移除屏幕外 **/
                    IntOffset(-it.width, 0)
                }

            }, label = ""
        ) {
            if (it) square() else round()
        }

        Button(onClick = { show = !show }) {
            Text(text = "测试")
        }
    }
}

裁切画面

配置状态的切换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Composable
fun TimeTest() {
    var count by remember { mutableIntStateOf(0) }

    Column(modifier = Modifier.fillMaxSize()) {
        Button(onClick = { count-- }) {
            Text(text = "-")
        }
        Spacer(modifier = Modifier.padding(20.dp))

        AnimatedContent(targetState = count, label = "", transitionSpec = {
            /** 使用 SizeTransform clip false 可以看到画面不裁切的效果,更加明显 **/
            if (targetState > initialState) { // 等价于 targetState isTransitioningTo initialState
                // 递增
                (slideInVertically { it } + fadeIn() togetherWith slideOutVertically { -it } + fadeOut()) using SizeTransform(clip = true)
            } else {
                // 递减
                (slideInVertically(animationSpec = tween(3000)) { -it } + fadeIn() togetherWith slideOutVertically(animationSpec = tween(3000)) { it } + fadeOut()) using SizeTransform(
                    clip = true
                )
            }
        }) {
            Box(
                modifier = Modifier
                    .background(Color.Green)
                    .padding(5.dp)
            ) {
                Text(text = "count $it", fontSize = 28.sp)
            }
        }

        Spacer(modifier = Modifier.padding(20.dp))
        Button(onClick = { count++ }) {
            Text(text = "+")
        }
    }
}

这段代码配置了一个 count 递增和递减的一个动画

  • count 递增的时候
    • 入场:初始状态以 fullHeight 的位置开始垂直方向滑入,且是一个淡入的效果
    • 出场:以初始状态垂直方向滑入到目标状态为 -fullHeight 的位置,且是一个淡出的效果
  • count 递减的时候:
    • 入场:初始状态以 - fullHeight 的位置开始垂直方向滑入,且是一个淡入的效果
    • 出场:以初始状态垂直方向滑入到目标状态为 fullHeight 的位置,且是一个淡出的效果

同时在递增和递减的过程中,都用 using 配置了 SizeTransform ,他可以理解为在这个入场和出场过程中,额外需要配置尺寸变化的,那么就使用它,这里我们配置了 clip 为 true,会对画面进行裁切,其实也就是默认情况下的效果,运行后看看效果

接下来我们再修改为 clip 为 false,也就是对画面不进行裁切,看看效果

设置为 clip 为 false,不裁切画面,可以发现可以看到动画位移的效果

targetContentZIndex

主要是用于控制组件的层级关系,默认值是 0f,数值较大的控件会覆盖较小的控件,如果数值相同,目标状态控件在最上面

它的场景,一般是用于想让动画在切换过程中,想让某一个控件无论是入场或者是退场动画中,控件一直在最上层显示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Composable
fun test2() {
    var show by remember { mutableStateOf(true) }

    /** 控制内容显示 **/
    Column(modifier = Modifier.fillMaxSize()) {
        // 修改 targetContentZIndex 的情况
        AnimatedContent(
            targetState = show, transitionSpec = {

                /** 目标状态是 true 的时候,这里动画的目标状态其实就是 square() 显示 **/
                if (targetState) {
                    (fadeIn(tween(3000)) togetherWith fadeOut(tween(6000))).apply {
                        /** 配置出场的动画 **/
                        /** 绘制顺序,小的数值后绘制,大的先绘制,那么大的就会被盖在小的上面 **/
                        /** 这样配置,就是 square 显示的时候,square 后绘制,square 一只在最底层,所以红色的会一直盖在上面 **/
                        targetContentZIndex = -1f
                    }
                } else fadeIn(tween(durationMillis = 3000)) togetherWith fadeOut(tween(3000))

            }, label = ""
        ) {
            if (it) square() else round()
        }
		Spacer(modifier = Modifier.height(20.dp))
         // 默认情况下
        AnimatedContent(
            targetState = show, transitionSpec = {
                /** 目标状态是 true 的时候,这里动画的目标状态其实就是 square() 显示 **/
                if (targetState) {
                    (fadeIn(tween(durationMillis = 3000)) togetherWith fadeOut(tween(3000)))
                } else (fadeIn(tween(durationMillis = 3000)) togetherWith fadeOut(tween(3000))) using SizeTransform()
            }, label = ""
        ) {
            if (it) square() else round()
        }

        Button(onClick = { show = !show }) {
              Text(text = "测试$show")
        }
    }
}

这段代码是使用 show 控制正方形和圆角矩形的切换效果的, true 状态的时候显示的是正方形,false 的时候显示圆角矩形, 所以正常情况下

  • true 到 false 的状态转变是,正方形慢慢消失,圆角矩形慢慢显示,且圆角矩形慢慢的盖在正方形上面
  • false 到 true 的状态转变是,圆角矩形慢慢消失,正方形慢慢显示,且正方形慢慢的盖在圆角矩形上面

而如果我想让正方形出现的时候,圆角矩形一直盖在正方形的上面,直到动画结束才消失,那么就可以修改正方形的 targetContentZIndex 比圆角矩形的小,这种状态的转变也就是 false 到 true 的转变

Modifier.animateEnterExit

为子项单独设置进入和退出的动画, animateEnterExit 外层必须是 AnimatedVisibility

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Composable
fun anim1() {
    var visible by remember {
        mutableStateOf(false)
    }
    AnimatedVisibility(
       //        visible = visible, enter = fadeIn(tween(durationMillis = 2000)), exit = fadeOut(tween(durationMillis = 2000))
        visible = visible, enter = EnterTransition.None, exit = ExitTransition.None
    ) {
        Box(
            Modifier
                .fillMaxSize()
                .background(Color.Gray)
        ) {
            Box(
                Modifier
                    .align(Alignment.Center)
                    .animateEnterExit(
                        enter = slideInVertically(tween(durationMillis = 2000)), exit = slideOutVertically(tween(durationMillis = 2000))
                    )
                    .sizeIn(minWidth = 64.dp, minHeight = 64.dp)
                    .background(Color.Green)
            ) 
        }
    }

    LaunchedEffect(key1 = Unit, block = {
        delay(2500)
        visible = true
    })
}

这个代码在控件显示的时候,同时子组件 box 设置了入场动画垂直滑入, 效果是这样的

如果不想要 AnimatedVisibility 的默认动画,就可以设置 NONE,这样就是单独使用子组件动画了

Modifier.animateContentSize

使用 animateContentSize,可让 Compose 组件大小发生变化的时候,具备动画的效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Composable
fun anim2() {
    var maxLines by remember { mutableIntStateOf(2) }

    Column {
        Box(
            modifier = Modifier
                .padding(20.dp)
                .fillMaxWidth()
                .background(Color.Gray)
                .animateContentSize(animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy))
        ) {
            Text(
                text = "111...",
                maxLines = maxLines
            )
        }

        Button(onClick = { maxLines = 5 }) {
            Text(text = "展开") 
        }
    }
}

总结

AnimatedVisibility 是对单个控件做显示和隐藏的动画效果, CrossFade 是两个控件,而 AnimatedContent 是多个控件,其中 AnimatedContent 功能更加强大,可以对入场和出场动画做单独的配置。而它们其实也都是 Transition 动画,至此,关于 compose 所有关于动画的设置都基本讲完了