在这里插入图片描述

Jetpack Compose 提供了一系列功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。这一系列文章会逐个介绍所有的动画 API,通过最直观的 Demo 示例,手把手教你怎么写动画以及带你了解动画背后的原理。


📑 手把手教你写 Compose 动画 - - 状态转移型动画 API:animate*AsState()

📑 手把手教你写 Compose 动画 - - 流程定制型动画 API:Animatable()

📑 手把手教你写 Compose 动画 - - 讲的不能再细的 AnimationSpec 动画规范

📑 手把手教你写 Compose 动画 - - 过渡动画 API:Transition

📑 手把手教你写 Compose 动画 - - 显示与消失 API:AnimatedVisibility

📑 手把手教你写 Compose 动画 - - 简单页面切换动画 API:Crossfade

📑 手把手教你写 Compose 动画 - - 更强大的多组件切换动画 API:AnimatedContent

📑 手把手教你写 Compose 动画 - - 组件大小变化 API:animateContentSize



📓 动画图表


在每一篇文章开头,我都会放一张 Compose 动画 API 的图表,以便你有最直观的感受。

在这里插入图片描述


📓 Transition


Transition 是 Compose 中实现过渡动画的关键 API 。所谓过渡动画,即当依赖的某个状态发生改变时连锁发生的一系列动画效果。

前面我们所提到的 animate*AsState 与 Animatable 都是针对一个属性(比如 offset 偏移)进行变换的,而 Transition 允许开发者将多个属性数值绑定到一个状态,当这个状态发生改变时,多个属性同时进行变换。

还是那句话:探索新技术的最佳方式是尝试它们,我们先构建一个简单场景:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Column (
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Image(
                    painter = painterResource(R.drawable.cr7),
                    contentDescription = null,
                    modifier = Modifier
                        .size(90.dp)
                        .clip(shape = CircleShape)
                        .border(color = Color.Red, shape = CircleShape, width = 3.dp)
                )

                Button(
                    onClick = {}
                ) {
                    Text(text = "切换")
                }
            }
        }
    }
}

这段代码极其简单:一个 Image,一个 Button,效果如下:


在这里插入图片描述


现在我们假设一个需求场景:

  1. 图片大小 size 需要变化:小图片(90dp)、大图片(130dp)
  2. 图片边框颜色 color 需要变化:绿色、红色
  3. 对应关系:小图片绿色边框,大图片红色边框

如果要实现这个需求,你会怎么做?目前我们掌握的动画仅有 animate*AsState() 和 Animatable.animateTo(),不如我们先用这两个动画 API 试试效果?



📚 animate*AsState()


如果我们用 animate*AsState() 来实现这个需求,代码可以这么写:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var bigPic by remember { mutableStateOf(false) }
            val size by animateDpAsState(if (bigPic) 130.dp else 90.dp, label = "")
            val borderColor by animateColorAsState(if (bigPic) Color.Red else Color.Green, label = "")
            Column (
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Image(
                    painter = painterResource(R.drawable.cr7),
                    contentDescription = null,
                    modifier = Modifier
                        .size(size)
                        .clip(shape = CircleShape)
                        .border(color = borderColor, shape = CircleShape, width = 3.dp)
                )

                Button(
                    onClick = { bigPic = !bigPic}
                ) {
                    Text(text = "切换")
                }
            }
        }
    }
}

你只要认真看过【 animate*AsState 用法 】这篇文章,看这段代码肯定 so easy。

我们需要定义两个 animateDpAsState,分别控制图片大小和文字颜色,效果如下:

在这里插入图片描述



📚 Animatable.animateTo


现在我们再来用 Animatable.animateTo 实现这个需求:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var bigPic by remember { mutableStateOf(false) }
            // Size Animatable
            val size = remember(bigPic) { if (bigPic) 130.dp else 90.dp }
            val sizeAnim = remember { Animatable(size, Dp.VectorConverter) }
            LaunchedEffect(bigPic) {
                sizeAnim.animateTo(size)
            }
            // Color Animatable
            val borderColor = remember(bigPic) { if (bigPic) Color.Red else Color.Green}
            val borderColorAnim = remember { Animatable(borderColor) }
            LaunchedEffect(bigPic) {
                borderColorAnim.animateTo(borderColor)
            }

            Column (
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Spacer(modifier = Modifier.height(100.dp))
                Image(
                    painter = painterResource(R.drawable.cr7),
                    contentDescription = null,
                    modifier = Modifier
                        .size(sizeAnim.value)
                        .clip(shape = CircleShape)
                        .border(color = borderColorAnim.value, shape = CircleShape, width = 3.dp)
                )

                Button(
                    onClick = { bigPic = !bigPic}
                ) {
                    Text(text = "切换")
                }
            }
        }
    }
}

你只要认真看过【 Animatable 用法 】这篇文章,看这段代码肯定也是 so easy。

我们需要定义两个 Animatable,并且需要启动两个协程,分别控制图片大小和文字颜色,效果如下:

在这里插入图片描述



📚 updateTransition


重点来了,现在我们开始讲解如何用 Transition 实现这个动画效果。

  1. 首先我们需要一个状态,状态可以是任何数据类型。我们通常会自定义一个枚举类型:
private enum class ImageState {
    Small, Large
}
  1. 现在我们再创建一个处理状态的变量:
var imageState by remember { mutableStateOf(ImageState.Small) }
  1. 创建 Transition 对象

Compose 中是通过 updateTransition() 函数来创建 Transition 对象,我们来看下 updateTransition() 函数:

@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T>

它有两个参数:

  1. targetState:状态变量,当它被更改时,动画会进行。
  2. label:动画的标签。
compose
compose - Docker Compose是一个用于定义和运行多容器Docker应用程序的工具,通过Compose文件格式简化应用部署过程。

这里的状态就是我们之前定义的:imageState,所以我们可以像下面这样写:

val transition = updateTransition(targetState = imageState, label = "ImageState Transition")

updateTransition() 会返回一个 Transition 对象

现在我们可以使用某个 animate* 扩展函数 来定义此过渡效果中的子动画。为每个状态指定目标值。这些 animate* 函数会返回一个动画值,在动画播放过程中,当使用 updateTransition 更新过渡状态时,该值将逐帧更新。

  1. 定制边框颜色过渡
val borderColor by transition.animateColor(label = "ImageState Color Transition") {
    when (it) {
        ImageState.Small -> Color.Green
        ImageState.Large -> Color.Magenta
    }
}
  1. 定制图片尺寸过渡
val size by transition.animateDp(label = "ImageState Size Transition") {
    when (it) {
        ImageState.Small -> 90.dp
        ImageState.Large -> 130.dp
    }
}

我们为每个属性状态(borderColor、size)声明了其在不同状态(ImageState.Small、ImageState.Large)时所对应的值,当过度动画所依赖状态(imageState)发生改变时,其中每个属性状态都会得到相应的更新。

  1. 应用到组件上(完整代码)

接下来,我们只需将创建的属性状态应用到我们的组件中即可:

private enum class ImageState {
    Small, Large
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var imageState by remember { mutableStateOf(ImageState.Small) }
            val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
            val borderColor by transition.animateColor(label = "ImageState Color Transition") {
                when (it) {
                    ImageState.Small -> Color.Green
                    ImageState.Large -> Color.Magenta
                }
            }

            val size by transition.animateDp(label = "ImageState Size Transition") {
                when (it) {
                    ImageState.Small -> 90.dp
                    ImageState.Large -> 130.dp
                }
            }

            Column (
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Image(
                    painter = painterResource(R.drawable.cr7),
                    contentDescription = null,
                    modifier = Modifier
                        .size(size)
                        .clip(shape = CircleShape)
                        .border(color = borderColor, shape = CircleShape, width = 3.dp)
                )

                Button(
                    onClick = {
                        imageState = if (imageState == ImageState.Small) {
                            ImageState.Large
                        } else {
                            ImageState.Small
                        }
                    }
                ) {
                    Text(text = "切换")
                }
            }
        }
    }
}

效果如下:

在这里插入图片描述


📓 灵魂思考


对于这么一个简单的需求,我们同时用 animate*AsState()AnimatableTransition 都可以实现。

那么我们就该思考一个问题了:

  1. 我们可以把 animate*AsState() 可以理解为 Animatable 的一种更简便直接的用法,Compose 创造出这个 API 的目的可以理解;
  2. 但是 Animatable 不是已经可以完美的实现了我们的需求了吗?为什么还要造一个 Transition 出来?

现在我们来解答这个疑惑:

  1. 首先我们回顾一下 AnimatableTransition 两种动画原理的核心思想。

Animatable

Animatable 是面向值的,在多个动画、多个状态的情况下存在不便于管理的问题。

var bigPic by remember { mutableStateOf(false) }

// Size Animatable
val size = remember(bigPic) { if (bigPic) 130.dp else 90.dp }
val sizeAnim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(bigPic) {
    sizeAnim.animateTo(size)
}

// Color Animatable
val borderColor = remember(bigPic) { if (bigPic) Color.Red else Color.Green}
val borderColorAnim = remember { Animatable(borderColor) }
LaunchedEffect(bigPic) {
    borderColorAnim.animateTo(borderColor)
}

针对 size 和 borderColor,我们要创建两个 Animatable,而且还要启动两个协程,如果我们还有多个其他动画,就要像下面这么写:

var bigPic by remember { mutableStateOf(false) }

// Size Animatable
val size = remember(bigPic) { if (bigPic) 130.dp else 90.dp }
val sizeAnim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(bigPic) {
    sizeAnim.animateTo(size)
}

// Color Animatable
val borderColor = remember(bigPic) { if (bigPic) Color.Red else Color.Green}
val borderColorAnim = remember { Animatable(borderColor) }
LaunchedEffect(bigPic) {
    borderColorAnim.animateTo(borderColor)
}

// Background Animatable
val background = remember(bigPic) { ... }
val backgroundAnim = remember { Animatable(background) }
LaunchedEffect(bigPic) {
	backgroundAnim.animateTo(background)
}

// Alpha Animatable
val alpha = remember(bigPic) { ... }
val alphaAnim = remember { Animatable(alpha) }
LaunchedEffect(bigPic) {
	alphaAnim.animateTo(alpha)
}

// .....

你就要不停的启动协程,然后写一堆结构差不多的代码,这还没有算上 bigPic 状态,如果你新增了其他类似 bigPic 的状态,还需要添加更多的状态片段逻辑…


Transition

Transition 是面向状态的,多个动画可以共用一个状态,能够做到统一的管理。

var imageState by remember { mutableStateOf(ImageState.Small) }
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")

val borderColor by transition.animateColor(label = "ImageState Color Transition") {
    when (it) {
        ImageState.Small -> Color.Green
        ImageState.Large -> Color.Magenta
    }
}

val size by transition.animateDp(label = "ImageState Size Transition") {
    when (it) {
        ImageState.Small -> 90.dp
        ImageState.Large -> 130.dp
    }
}

updateTransition 只会创建一次协程,而且只需要根据一种状态的变化,就可以控制不同的动画效果。

如果我们还有多个其他动画,就可以这么写:

var imageState by remember { mutableStateOf(ImageState.Small) }
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")

val borderColor by transition.animateColor(label = "ImageState Color Transition") {
    when (it) {
        ImageState.Small -> Color.Green
        ImageState.Large -> Color.Magenta
    }
}

val size by transition.animateDp(label = "ImageState Size Transition") {
    when (it) {
        ImageState.Small -> 90.dp
        ImageState.Large -> 130.dp
    }
}

val background by transition.animateColor(label = "ImageState Color Transition") {
    when (it) {
        ImageState.Small -> Color.Yellow
        ImageState.Large -> Color.Magenta
    }
}

val alpha by transition.animateFloat(label = "ImageState Alpha Transition") {
    when (it) {
        ImageState.Small -> 0.3f
        ImageState.Large -> 0.5f
    }
}

结构清晰明了,非常方便。

  1. Transition 除了代码结构和逻辑清晰的优势以外,它还有个更牛逼的功能:支持 Compose 动画预览!

什么是动画预览呢?我们代码写到现在了,不知道你有没有注意到所有的动画 API 都加上了一个 label 标签参数:

val transition = updateTransition(targetState = imageState, label = "ImageState Transition")

val borderColor by transition.animateColor(label = "ImageState Color Transition") {
    when (it) {
        ImageState.Small -> Color.Green
        ImageState.Large -> Color.Magenta
    }
}

这个 label 有什么用呢?一会你就知道了。

现在我们看看怎么打开这个 Compose 动画预览功能:

  1. 首先我们把代码调整拆到一个自定义 Composable 函数中,如下:
private enum class ImageState {
    Small, Large
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            TransitionPreview()
        }
    }
}

@Composable
fun TransitionPreview() {
    var imageState by remember { mutableStateOf(ImageState.Small) }
    val transition = updateTransition(targetState = imageState, label = "ImageState Transition")

    val borderColor by transition.animateColor(label = "ImageState Color Transition") {
        when (it) {
            ImageState.Small -> Color.Green
            ImageState.Large -> Color.Magenta
        }
    }

    val size by transition.animateDp(label = "ImageState Size Transition") {
        when (it) {
            ImageState.Small -> 90.dp
            ImageState.Large -> 130.dp
        }
    }

    Column(
        modifier = Modifier.fillMaxWidth(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Spacer(modifier = Modifier.height(100.dp))
        Image(
            painter = painterResource(R.drawable.cr7),
            contentDescription = null,
            modifier = Modifier
                .size(size)
                .clip(shape = CircleShape)
                .border(color = borderColor, shape = CircleShape, width = 3.dp)
        )

        Button(
            onClick = {
                imageState = if (imageState == ImageState.Small) {
                    ImageState.Large
                } else {
                    ImageState.Small
                }
            }
        ) {
            Text(text = "切换")
        }
    }
}
  1. 现在我们给 TransitionPreview() 函数添加 @Preview 注解:
@Preview
@Composable
fun TransitionPreview() {
}

加上这个注解,我们就可以直接在 Android Studio 界面右侧预览这个 @Composable 可组合项的效果,而不需要运行到模拟机或者真机。

在这里插入图片描述

现在还只是组件的预览界面,我们可以点击 Start Animation Preview 按钮进入动画预览界面:

在这里插入图片描述

进入之后会是下面这个样子:

在这里插入图片描述

我们现在来分析一下这个动画预览界面,注意看图:

在这里插入图片描述

红色框框显示的是什么?不就是对应着我们创建 Transition 时候填的 label 么?

val transition = updateTransition(targetState = imageState, label = "ImageState Transition")

我们现在点击箭头展开它:

在这里插入图片描述

现在你知道为什么要加 label 了吧?动画预览界面可操作性很强,你可以拖动进度条到动画的任意位置,还能互换动画的初始状态和目标状态,设置动画的倍速等,这个自行探索吧。

在这里插入图片描述

我估计这个时候,你可能就会想一个问题了,难道 animate*AsState 不支持,它不是也加了 label 了吗?我们试一下:

在这里插入图片描述

animate*AsState 虽然支持添加 label,但实际上没有任何动画可以调节。

推荐内容
阅读全文
AI总结
GitHub 加速计划 / compose / compose
86
5
下载
compose - Docker Compose是一个用于定义和运行多容器Docker应用程序的工具,通过Compose文件格式简化应用部署过程。
最近提交(Master分支:7 个月前 )
51907d9f Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> 2 天前
a3f88a0a Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com> 2 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐