17.4 Compose 动画(四) — AnimatedVisibility、Crossfade、AnimatedContent、animateContent

491 阅读1分钟

都是高级 API,有了前面的铺垫,就看看参数和效果。

AnimatedVisibility

AnimatedVisibility 是带有内容显示/消失动画的容器组件,内部动画使用 Transition 实现。

消失时是直接从 Compose 树中移除 ,显示再添加回来, 所以显示/消失会影响子组件的布局。

显示/消失

需要监听动画状态的情况使用 VisibleState:MutableTransitionState , 不需要的直接使用 Boolean 控制显示/消失。

    @Composable
fun AnimatedVisibilityDemo(){
    var visible by remember { mutableStateOf(true) }
    Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        AnimatedVisibility(visible = visible) {
            Icon(imageVector = Icons.Default.Favorite, contentDescription = "")
        }
        Button(onClick = { visible = !visible}) {
            Text(text = "Change Visibility")
        }
    }
}

@Composable
fun AnimatedVisibilityDemo2(){
    val visibleState = remember { MutableTransitionState(true) }
    Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        AnimatedVisibility(visibleState = visibleState) {
            Icon(imageVector = Icons.Default.Favorite, contentDescription = "")
        }
        Button(onClick = { visibleState.targetState = !visibleState.currentState}) {
            Text(text = "Current :${infoFromVisibleState(visibleState)}")
        }
    }
}
fun infoFromVisibleState(visibleState: MutableTransitionState<Boolean>): String = when{
    visibleState.isIdle && visibleState.currentState -> "Visible"
    !visibleState.isIdle && visibleState.currentState -> "Disappearing"
    visibleState.isIdle && !visibleState.currentState -> "Invisible"
    else -> "Appearing"
}

Untitled.gif

显示/消失动画

EnterTransition、ExitTransition 用来设置显示隐藏动画,Compose 提供了下面四种类型的 Transition 子动画,同类型的子动画可以通过 + 组合在一起。动画效果可以查看官网

EnterTransitionExitTransition
fadefadeInfadeOut
scalescaleInscaleOut
slideslideIn, slideInHorizontally, slideInVerticallyslideOut, slideOutHorizontally, slideOutVertically
expand/shrinkexpandIn, expandHorizontally, expandVerticallyshrinkOut, shrinkHorizontally, shrinkVertically
        AnimatedVisibility(
            visible = visible,
            enter = slideInHorizontally(
                animationSpec = spring(Spring.DampingRatioHighBouncy)
            ){ fullWidth ->
                -fullWidth
            } + fadeIn(tween(1000), 0.3f),
            exit = slideOutVertically() + fadeOut()
        )    

Untitled.gif

子控件可以使用 Modifier.animateEnterExit() 单独设置动画

        AnimatedVisibility(
            visible = visible,
            enter = slideInHorizontally(
                animationSpec = spring(Spring.DampingRatioHighBouncy)
            ){ fullWidth -> -fullWidth } + fadeIn(tween(1000), 0.3f),
            exit = slideOutVertically() + fadeOut()
        ) {
            Box(modifier = Modifier.size(100.dp).background(Color.Cyan)){
                Icon(modifier = Modifier.align(Alignment.Center)
                    .animateEnterExit(
                        enter = slideInVertically { -it * 2 },
                        exit = slideOutHorizontally { it * 2 }
                    )
                    ,
                    imageVector = Icons.Default.Favorite, contentDescription = "")
            }
}    

Untitled.gif

AnimatedVisibilityScope

content: @Composable() AnimatedVisibilityScope.() -> Unit   

AnimatedVisibility 中 content 在 AnimatedVisibilityScope 中,这里可以通过 this.transition 获取到 AnimatedVisibility 内部的 Transition 对象的 ,那么我们除了可以直接拿到 transition 里当前属性值以外,还可以向 transition 中添加子动画。

    Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        AnimatedVisibility(
            visible = visible,
            enter = slideInHorizontally(
                animationSpec = spring(Spring.DampingRatioHighBouncy)
            ){ fullWidth -> -fullWidth } + fadeIn(tween(1000), 0.3f),
            exit = slideOutVertically() + fadeOut()
        ) {
            val childAnim = this.transition.animateColor(label = "bgColor") { enterExitState ->
                when (enterExitState) {//为 3 中枚举状态提供 targetValue
                    EnterExitState.PreEnter -> Color.Yellow
                    EnterExitState.Visible -> Color.Cyan
                    EnterExitState.PostExit -> Color.Magenta
                }
            }
            Box(modifier = Modifier
                .size(100.dp)
                .background(childAnim.value)){
                Icon(modifier = Modifier
                    .align(Alignment.Center)
                    .animateEnterExit(
                        enter = slideInVertically { -it * 2 },
                        exit = slideOutHorizontally { it * 2 }
                    )
                    ,
                    imageVector = Icons.Default.Favorite, contentDescription = "")
            }
            
        }
        Button(onClick = { visible = !visible}) {
            Text(text = "Change Visibility ")
        }
}    

Untitled.gif

三种实现的默认动画不一样

AnimatedVisibility 有三种 Composable 实现,每种实现的默认动画是不一样的。

@Composable
fun AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = fadeOut() + shrinkOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

@Composable
fun ColumnScope.AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = expandVertically() + fadeIn(),
    exit: ExitTransition = shrinkVertically() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

@Composable
fun RowScope.AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = expandHorizontally() + fadeIn(),
    exit: ExitTransition = shrinkHorizontally() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

如果 AnimatedVisibility 是 Column  的直接或间接子组件,那么它就是 ColumnScope.AnimatedVisibility,且作为间接子组件必须使用 this@Column.AnimatedVisibility

Column(Modifier.fillMaxWidth()) {
    Box(modifier = Modifier.size(100.dp).background(Color.Red)){
        this@Column.AnimatedVisibility(visibleState = visibleState) {
            Icon(Icons.Default.Favorite,"")
        }
    }
}    

Crossfade  和 AnimatedContent

Crossfade

Crossfade 也是底层由 Transitiion 实现的容器,容器中内容使用淡入淡出效果切换。没有其他花里胡哨的配置,就这么朴实无华

enum class CrossfadeContent{
    One,Two;
    fun next() = if (this == One) Two else One
}

@Composable
fun CrossfadeDemo(){
    var curContent by remember { mutableStateOf(CrossfadeContent.One) }
    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        Crossfade(targetState = curContent) {
            if (it == CrossfadeContent.One){
                Box(modifier = Modifier.size(100.dp).background(Color.Cyan)){}
            }else{
                Box(modifier = Modifier.size(100.dp).background(Color.Magenta)){}
            }
        }
        Button(onClick = { curContent = curContent.next() }) {
            Text(text = "Switch")
        }
    }
}   

Untitled.gif

AnimatedContent 也是底层由 Transitiion 实现的容器,功能上跟 Crossfade 一样,但是它可以自定义切换动画效果。

transitionSpec: AnimatedContentScope<S>.() -> ContentTransform 参数需要返回一个 ContentTransform ,

ContentTransform 中指定了内容切换时的 EnterTransition 、 ExitTransition ,  EnterTransition 、 ExitTransition这两个在 AnimatedVisibility 的时候我们已经说过了。

可以使用下面两种方法来创建 ContentTrans

EnterTransition with ExitTransition  //1
(EnterTransition with ExitTransition).using(SizeTransform())//2  

content: @Composable() AnimatedVisibilityScope  这个跟 AnimatedVisibility 一样 ,所以也可以在 content 中拿到当前的 transition 对象。

Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
    AnimatedContent(
        targetState = curContent,
        transitionSpec = {
//                (slideInHorizontally{ -it } with slideOutHorizontally{ it })
            (slideInHorizontally{ -it } with slideOutHorizontally{ it }).using(SizeTransform(clip = false))
        }
    ) {  targetState ->
        if (targetState == CrossfadeContent.One){
            Box(modifier = Modifier
                .size(100.dp)
                .background(Color.Cyan)){}
        }else{
            Box(modifier = Modifier
                .size(100.dp)
                .background(Color.Magenta)){}
        }
    }
    Text(text = "using(SizeTransform(clip = false))")
    Button(onClick = { curContent = curContent.next() }) {
        Text(text = "Switch")
    }
}    

Untitled.gif

Untitled.gif

Modifier.animateContentSize

设置内容 Size 变化的动画效果。 animationSpec 参数设置动画效果 ,listener 监听在动画结束时可以获取到内容变化前后的 IntSize。

@Composable
fun AnimateContentSizeDemo(){
    var isExpanded by remember { mutableStateOf(false) }
    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = "很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长" ,
            maxLines = if (isExpanded) Int.MAX_VALUE else 2,
            modifier = Modifier
                .width(200.dp)
                .animateContentSize(
                    animationSpec = spring(Spring.DampingRatioHighBouncy)
                ) { initialValue, targetValue ->
                    Log.e(TAG, "AnimateContentSizeDemo: initialValue = $initialValue ; targetValue = $targetValue", )
                }
        )
        Button(onClick = { isExpanded = !isExpanded}) {
            Text(text = if (isExpanded) "收起" else "展开")
        }
    }
}    

Untitled.gif