JetpackCompose从入门到实战学习笔记12—在Compose中简单使用动画
1.动画简介:
Jetpack Compose 提供了一些功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。本文将介绍如何使用这些 API,以及根据您的动画场景应使用哪种 API。
2.动画分类:
概览
动画在现代移动应用中至关重要,其目的是实现自然流畅、易于理解的用户体验。许多 Jetpack Compose 动画 API 可以提供可组合函数,就像布局和其他界面元素一样;它们由使用 Kotlin 协程挂起函数构建的较低级别 API 提供支持。本指南将首先介绍可用于许多实际场景的高级别 API,接着介绍可为您提供进一步控制和自定义功能的低级别 API。
下面的图表可以帮助您确定要使用哪种 API 来实现您的动画效果。
3.AnimateAsState
代码如下:
@Preview
@Composable
fun AnimateAsStateDemo() {
var blue by remember { mutableStateOf(true) }
val color by animateColorAsState(
if (blue) Blue else Red,
animationSpec = spring(stiffness = Spring.StiffnessVeryLow),
finishedListener = {
blue = !blue
}
)
Column(Modifier.padding(16.dp)) {
Text("AnimateAsStateDemo")
Spacer(Modifier.height(16.dp))
Button(
onClick = { blue = !blue }
) {
Text("Change Color")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.size(128.dp)
.background(color)
)
}
}
4.效果预览如下:
5.AnimatedVisibility:
如果您要为出现和消失添加动画效果使用AnimatedVisibility
参数 | 类型 | 描述 |
visible | boolean | 是否可见 |
enter | EnterTransition | 进入动画 |
exit | EnterTransition | 退出动画 |
label | string | 标签 |
content | unit | 内容 |
代码如下:
@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun AnimateVisibilityDemo() {
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
Column(Modifier.padding(16.dp)) {
Text("AnimateVisibilityDemo")
Spacer(Modifier.height(16.dp))
Button(
onClick = { visible = !visible }
) {
Text(text = if (visible) "Hide" else "Show")
}
Spacer(Modifier.height(16.dp))
AnimatedVisibility(visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut())
{
Box(
Modifier
.size(128.dp)
.background(Blue)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
)
}
}
}
6.效果预览如下:
7.AnimatedContent
如果您要为内容添加淡入淡出效果使用AnimatedContent
代码如下:
@Preview
@Composable
fun AnimateContentSizeDemo(){
var extend by remember { mutableStateOf(false) }
Column(Modifier.padding(16.dp)) {
Text("AnimateContentSizeDemo")
Spacer(Modifier.height(16.dp))
Button(
onClick = { expend = !expend }
) {
Text(if (expend) "Shrink" else "Expand")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.background(Color.LightGray)
.animateContentSize()
) {
Text(
text = "animateContentSize() animates its own size when its child modifier (or the child composable if it is already at the tail of the chain) changes size. " +
"This allows the parent modifier to observe a smooth size change, resulting in an overall continuous visual change.",
fontSize = 16.sp,
textAlign = TextAlign.Justify,
modifier = Modifier.padding(16.dp),
maxLines = if (expend) Int.MAX_VALUE else 2
)
}
}
}
8.效果预览如下:
9.AnimatedContent例子2
代码:
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimationContentDemo() {
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.padding(vertical = 2.dp, horizontal = 8.dp)
.background(Color.LightGray)
.size(200.dp),
contentAlignment = Alignment.Center
) {
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "$targetCount",
color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center
)
}
}
}
}
10.效果预览:
11.Crossfade:
private enum class DemoScene {
Text, Icon
}
@SuppressLint("UnusedCrossFadeTargetStateParameter")
@Preview
@Composable
fun CrossFadeDemo() {
var scene by remember { mutableStateOf(DemoScene.Text) }
Column(Modifier.padding(16.dp)) {
Text("CrossFadeDemo")
Spacer(Modifier.height(16.dp))
Button(onClick = {
scene = when (scene) {
DemoScene.Text -> DemoScene.Icon
DemoScene.Icon -> DemoScene.Text
}
}) {
Text("toggle")
}
Spacer(Modifier.height(16.dp))
when (scene) {
DemoScene.Text ->
Crossfade(
targetState = scene,
animationSpec = tween(durationMillis = 1000)
) {
Text(text = "Phone", fontSize = 32.sp)
}
DemoScene.Icon ->
Crossfade(
targetState = scene,
animationSpec = tween(durationMillis = 1000)
) {
Icon(
imageVector = Icons.Default.Phone,
null,
modifier = Modifier.size(48.dp)
)
}
}
}
}
12.效果预览如下:
13.UpdateTransition:
private sealed class BoxState(val color: Color, val size: Dp) {
operator fun not() = if (this is Small) Large else Small
object Small : BoxState(Blue, 64.dp)
object Large : BoxState(Red, 128.dp)
}
@SuppressLint("UnusedTransitionTargetStateParameter")
@Preview
@Composable
fun UpdateTransitionDemo() {
var boxState: BoxState by remember { mutableStateOf(BoxState.Small) }
val transition = updateTransition(targetState = boxState, label = "")
Column(Modifier.padding(16.dp)) {
Text("UpdateTransitionDemo")
Spacer(Modifier.height(16.dp))
val color by transition.animateColor(label = "") {
boxState.color
}
val size by transition.animateDp(transitionSpec = {
if (targetState == BoxState.Large) {
spring(stiffness = Spring.StiffnessVeryLow)
} else {
spring(stiffness = Spring.StiffnessHigh)
}
}, label = "") {
boxState.size
}
Button(
onClick = { boxState = !boxState }
) {
Text("Change Color and size")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.size(size)
.background(color)
)
}
}
14.效果预览如下:
15.完整代码如下:
package com.example.composeanimationdemo
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Phone
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color.Companion.Blue
import androidx.compose.ui.graphics.Color.Companion.Red
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.composeanimationdemo.ui.theme.ComposeAnimationDemoTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeAnimationDemoTheme {
//AnimateAsStateDemo()
//AnimateVisibilityDemo()
//AnimateContentSizeDemo()
//CrossFadeDemo()
//UpdateTransitionDemo()
AnimationContentDemo()
}
}
}
@Preview
@Composable
fun AnimateAsStateDemo() {
var blue by remember { mutableStateOf(true) }
val color by animateColorAsState(
if (blue) Blue else Red,
animationSpec = spring(stiffness = Spring.StiffnessVeryLow),
finishedListener = {
blue = !blue
}
)
Column(Modifier.padding(16.dp)) {
Text("AnimateAsStateDemo")
Spacer(Modifier.height(16.dp))
Button(
onClick = { blue = !blue }
) {
Text("Change Color")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.size(128.dp)
.background(color)
)
}
}
private sealed class BoxState(val color: Color, val size: Dp) {
operator fun not() = if (this is Small) Large else Small
object Small : BoxState(Blue, 64.dp)
object Large : BoxState(Red, 128.dp)
}
@SuppressLint("UnusedTransitionTargetStateParameter")
@Preview
@Composable
fun UpdateTransitionDemo() {
var boxState: BoxState by remember { mutableStateOf(BoxState.Small) }
val transition = updateTransition(targetState = boxState, label = "")
Column(Modifier.padding(16.dp)) {
Text("UpdateTransitionDemo")
Spacer(Modifier.height(16.dp))
val color by transition.animateColor(label = "") {
boxState.color
}
val size by transition.animateDp(transitionSpec = {
if (targetState == BoxState.Large) {
spring(stiffness = Spring.StiffnessVeryLow)
} else {
spring(stiffness = Spring.StiffnessHigh)
}
}, label = "") {
boxState.size
}
Button(
onClick = { boxState = !boxState }
) {
Text("Change Color and size")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.size(size)
.background(color)
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Preview
@Composable
fun AnimateVisibilityDemo() {
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
Column(Modifier.padding(16.dp)) {
Text("AnimateVisibilityDemo")
Spacer(Modifier.height(16.dp))
Button(
onClick = { visible = !visible }
) {
Text(text = if (visible) "Hide" else "Show")
}
Spacer(Modifier.height(16.dp))
AnimatedVisibility(visible,
enter = slideInVertically {
// Slide in from 40 dp from the top.
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// Expand from the top.
expandFrom = Alignment.Top
) + fadeIn(
// Fade in with the initial alpha of 0.3f.
initialAlpha = 0.3f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut())
{
Box(
Modifier
.size(128.dp)
.background(Blue)
.animateEnterExit(
// Slide in/out the inner box.
enter = slideInVertically(),
exit = slideOutVertically()
)
)
}
}
}
@Preview
@Composable
fun AnimateContentSizeDemo() {
var expend by remember { mutableStateOf(false) }
Column(Modifier.padding(16.dp)) {
Text("AnimateContentSizeDemo")
Spacer(Modifier.height(16.dp))
Button(
onClick = { expend = !expend }
) {
Text(if (expend) "Shrink" else "Expand")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.background(Color.LightGray)
.animateContentSize()
) {
Text(
text = "animateContentSize() animates its own size when its child modifier (or the child composable if it is already at the tail of the chain) changes size. " +
"This allows the parent modifier to observe a smooth size change, resulting in an overall continuous visual change.",
fontSize = 16.sp,
textAlign = TextAlign.Justify,
modifier = Modifier.padding(16.dp),
maxLines = if (expend) Int.MAX_VALUE else 2
)
}
}
}
private enum class DemoScene {
Text, Icon
}
@SuppressLint("UnusedCrossFadeTargetStateParameter")
@Preview
@Composable
fun CrossFadeDemo() {
var scene by remember { mutableStateOf(DemoScene.Text) }
Column(Modifier.padding(16.dp)) {
Text("CrossFadeDemo")
Spacer(Modifier.height(16.dp))
Button(onClick = {
scene = when (scene) {
DemoScene.Text -> DemoScene.Icon
DemoScene.Icon -> DemoScene.Text
}
}) {
Text("toggle")
}
Spacer(Modifier.height(16.dp))
when (scene) {
DemoScene.Text ->
Crossfade(
targetState = scene,
animationSpec = tween(durationMillis = 1000)
) {
Text(text = "Phone", fontSize = 32.sp)
}
DemoScene.Icon ->
Crossfade(
targetState = scene,
animationSpec = tween(durationMillis = 1000)
) {
Icon(
imageVector = Icons.Default.Phone,
null,
modifier = Modifier.size(48.dp)
)
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimationContentDemo() {
Row {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Add")
}
AnimatedContent(
targetState = count,
transitionSpec = {
// Compare the incoming number with the previous number.
if (targetState > initialState) {
// If the target number is larger, it slides up and fades in
// while the initial (smaller) number slides up and fades out.
slideInVertically { height -> height } + fadeIn() with
slideOutVertically { height -> -height } + fadeOut()
} else {
// If the target number is smaller, it slides down and fades in
// while the initial number slides down and fades out.
slideInVertically { height -> -height } + fadeIn() with
slideOutVertically { height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should
// be displayed out of bounds.
SizeTransform(clip = false)
)
}
) { targetCount ->
Spacer(modifier = Modifier.height(10.dp))
Box(
modifier = Modifier
.padding(vertical = 2.dp, horizontal = 8.dp)
.background(Color.LightGray)
.size(200.dp),
contentAlignment = Alignment.Center
) {
Spacer(modifier = Modifier.height(10.dp))
Text(
text = "$targetCount",
color = MaterialTheme.colors.secondary,
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center
)
}
}
}
}
}
16.完整效果预览:
![在这里插入图片描述](
17.项目demo源码地址如下:
https://gitee.com/jackning_admin/compose-animation-demo
18.总结:
以上就是今天的内容,可以看出使用compose实现动画很简单方便,效果也还可以,只不过还有一些复杂的动画需要后面慢慢研究,希望compose越来越完善,体检更优,一直往前走.