Crossfade & AnimatedContent

Crossfade

AnimatedVisibility 能够为同一内容的呈现和消失增加动画作用,那内容切换(一个内容消失,另一个内容呈现)该怎么做动画呢?总不能写两遍 AnimatedVisibility 吧,这时候就需要用到 Crossfade 了。Crossfade 能在两个布局内容切换时增加淡出淡入动画作用。运用也非常简单:

var currentShape by remember { mutableStateOf(Shape.Circle) }
Crossfade(targetState = currentShape) { currentShape ->
    when (currentShape) {
        Shape.Circle -> SmallCircle()
        Shape.Square -> BigBox()
    }
}
Jetpack Compose动画笔记6——Crossfade & AnimatedContent

第一个必填参数是方针状况,第二个必填参数是内容,能够依据不同状况来加载不同的内容,状况改变导致内容切换时,Crossfade 会为消失的内容增加淡出动画,为呈现的内容增加淡入动画。

@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 也是基于 Transition 完成的,调用的是 transition.Crossfade() 办法。

淡入淡出说白了便是透明度动画,咱们能够运用参数 animationSpec: FiniteAnimationSpec<Float> 来自定义透明度动画的规格,例如动画时长、动画曲线等等。

呃…产品那边说不想要淡入淡出的切换作用,能不能整个别的切换作用,右滑出左滑入的那种也行啊…… 不好意思,不行,Crossfade 只能做淡入淡出的切换作用,毕竟它的名字就叫 Cross Fade 嘛,不过咱们能够用 AnimatedContent 来完成其他的内容切换作用。

AnimatedContent

AnimatedContent 会在内容依据方针状况发生变化时,为内容增加动画作用。

能够把 AnimatedContent 看作是一个高档版本的 Crossfade,Crossfade 只能做淡入淡出的切换作用,而 AnimatedContent 能够做更多的切换作用。用法和 Crossfade 差不多,直接把上面代码的 Crossfade 换成 AnimatedContent

var currentShape by remember { mutableStateOf(Shape.Circle) }
AnimatedContent(targetState = currentShape) { currentShape ->
    when (currentShape) {
        Shape.Circle -> SmallCircle()
        Shape.Square -> BigBox()
    }
}
Jetpack Compose动画笔记6——Crossfade & AnimatedContent
@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
) {
    // 一样是基于 Transition 完成的
    val transition = updateTransition(targetState = targetState, label = label)
    transition.AnimatedContent(modifier, transitionSpec, contentAlignment, contentKey, content = content)
}

要点看一下 AnimatedContent() 的参数 transitionSpec,毕竟咱们便是奔着“更多的内容切换作用”来的,而这个参数很明显便是用来装备内容的切换作用。它的类型是 AnimatedContentTransitionScope<S>.() -> ContentTransform,感觉有点眼熟,和 Transition.animateXxx() 的参数 transitionSpec 差不多,都是函数参数类型,不过这个函数参数要求回来类型是 ContentTransform

class ContentTransform(
    val targetContentEnter: EnterTransition,
    val initialContentExit: ExitTransition,
    targetContentZIndex: Float = 0f,
    sizeTransform: SizeTransform? = SizeTransform()
)

ContentTransform 的结构函数能够看出,这是一个内容切换动画的装备类,能够装备 4 个方面的参数:

  • targetContentEnter: EnterTransition 进场动画;

  • initialContentExit: ExitTransition 进场动画;

  • targetContentZIndex: Float 用于指定进场内容的 z-index 值。默许情况下,进场的内容和进场的内容的 z-index 都是 0f,不过 Compose 规则:两个 z-index 值一样的内容,后被增加到界面上的内容会更靠前,所以默许的一次内容切换动画中,进场内容会显示在进场内容之上。

  • sizeTransform: SizeTransform 当进场内容和进场内容的巨细不一致时就会涉及到 Size 的转化动画。这个 Size 动画,到底是归于进场动画仍是进场动画的一部分呢?好像都归于,又好像都不归于。算了,爽性用一个独立参数来对这个 Size 动画进程进行装备吧。

    Crossfade 的淡出淡入是没有巨细转化动画的,能够比照一下:

    Jetpack Compose动画笔记6——Crossfade & AnimatedContent

能够通过结构函数创立一个 ContentTransform 实例,不过更常见的做法是运用 infix 函数 togetherWith()

infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)
// ContentTransform = EnterTransition togetherWith ExitTransition

别的顺带提一下,ContentTransform 有一个 infix 函数 using(),用于装备 SizeTransform

override infix fun ContentTransform.using(sizeTransform: SizeTransform?) = this.apply {
    this.sizeTransform = sizeTransform
}

现在能够回头看一下 AnimatedContent() 可选参数 transitionSpec 的默许值了:

transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
    (fadeIn(animationSpec = tween(220, delayMillis = 90)) +
     scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90))
    ).togetherWith(fadeOut(animationSpec = tween(90)))
}

进场动画是 [淡入 fadeIn] + [放大 scaleIn],进场动画是 [淡出 fadeOut],出进场延迟都是 90 ms。



Sample

现在咱们来着手试一试,现在有一个依据 ascill 码来切换字母方块的 AnimatedContent 代码块。

Jetpack Compose动画笔记6——Crossfade & AnimatedContent
var currentAsciiCode by remember { mutableStateOf(65) }
...
AnimatedContent(targetState = currentAsciiCode) { asciiCode ->
    val letterChar = Char(code = asciiCode)
    LetterBox(letter = letterChar)
}

想把这个默许作用改为 “左面滑入,右边滑出”,像纸张一样层层堆叠递进:

var currentAsciiCode by remember { mutableStateOf(65) }
...
AnimatedContent(
    targetState = currentAsciiCode,
    transitionSpec = {
        (fadeIn() + slideInHorizontally { fullWidth -> -fullWidth }) togetherWith
        (fadeOut() + slideOutHorizontally { fullWidth -> fullWidth })
    }) { asciiCode ->
    val letterChar = Char(code = asciiCode)
    LetterBox(letter = letterChar)
}
Jetpack Compose动画笔记6——Crossfade & AnimatedContent

进场动画设为 fadeIn() + slideInHorizontally { fullWidth -> -fullWidth },淡入,而且初始偏移方位就在容器左边,进场动画便是相反的。

Jetpack Compose动画笔记6——Crossfade & AnimatedContent

不过!咱们这个场景是字母切换,A -> Z,这个切换场景是有次序的,当咱们反过来的时候就会发现问题了:

Jetpack Compose动画笔记6——Crossfade & AnimatedContent

反过来从 Z -> A 的时候,进场进场动画仍是按照之前的设置,动画作用就不符合预期的,感觉很奇怪。解决办法也很简单,咱们依据动画的初始状况和方针状况来判断是正向切换仍是反向切换,然后依据切换方历来设置不同的进场进场动画就 OK 了:

var currentAsciiCode by remember { mutableStateOf(65) }
...
AnimatedContent(
    targetState = currentAsciiCode,
    transitionSpec = { // 具有 AnimatedContentTransitionScope 上下文,能够获取 initialState 和 targetState
        if (targetState > initialState) { // A -> Z
            (fadeIn() + slideInHorizontally { fullWidth -> -fullWidth }) togetherWith
            (fadeOut() + slideOutHorizontally { fullWidth -> fullWidth })
        } else { // Z -> A
            (fadeIn() + slideInHorizontally { fullWidth -> fullWidth }) togetherWith
            (fadeOut() + slideOutHorizontally { fullWidth -> -fullWidth })
        }
    }) { asciiCode ->
    val letterChar = Char(code = asciiCode)
    LetterBox(letter = letterChar)
}

哎,Okey,现在完美了。原来参数 transitionSpec 类型设计成函数参数,便是为了让咱们能够依据不同的场景来动态装备不同的动画作用啊。

别的,有两个很有用的辅佐函数 slideIntoContainer(towards = ...)slideOutOfContainer(towards = ...),这两个是 AnimatedContentTransitionScope 的拓宽函数,能够快速创立滑入滑出容器的动画作用,只需咱们提供方向,而不必咱们手动核算初始和方针偏移量

运用它俩能够代替上面咱们写的 slideInHorizontally { }slideOutHorizontally { }

var currentAsciiCode by remember { mutableStateOf(65) }
...
AnimatedContent(
    targetState = currentAsciiCode,
    transitionSpec = { // 具有 AnimatedContentTransitionScope 上下文,能够获取 initialState 和 targetState
        if (targetState > initialState) { // A -> Z
            (fadeIn() + slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right)) togetherWith
            (fadeOut() + slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Right))
        } else { // Z -> A
            (fadeIn() + slideIntoContainer(towards = AnimatedContentTransitionScope.SlideDirection.Left)) togetherWith
            (fadeOut() + slideOutOfContainer(towards = AnimatedContentTransitionScope.SlideDirection.Left))
        }
    }) { asciiCode ->
    ...
}

上面的比如中,进场内容和进场内容都是等巨细的,假如进场内容和进场内容的巨细不一致,运用传统方法 slideInHorizontally { fullWidth -> -fullWidth } 核算偏移量就会呈现问题:

Jetpack Compose动画笔记6——Crossfade & AnimatedContent
Jetpack Compose动画笔记6——Crossfade & AnimatedContent

进场内容是大方块,进场内容是小方块,进场内容的初始方位就在容器左边,进场内容的初始方位就在容器右侧,而 slideInHorizontally 传入的 fullWidth 便是当时容器的宽,也便是小方块的宽,这样的话,偏移量就会偏小。

假如运用 slideIntoContainer()slideOutOfContainer(),就不会呈现这个问题,由于这两个函数会主动核算进场内容和进场内容的巨细,取其中较大的那个作为偏移量的基准值,这样就不会呈现偏移量偏小的问题了。

Jetpack Compose动画笔记6——Crossfade & AnimatedContent
Jetpack Compose动画笔记6——Crossfade & AnimatedContent