JetpackCompose从入门到实战学习笔记12—在Compose中简单使用动画

1.动画简介:

Jetpack Compose 提供了一些功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。本文将介绍如何使用这些 API,以及根据您的动画场景应使用哪种 API。

2.动画分类:

概览

动画在现代移动应用中至关重要,其目的是实现自然流畅、易于理解的用户体验。许多 Jetpack Compose 动画 API 可以提供可组合函数,就像布局和其他界面元素一样;它们由使用 Kotlin 协程挂起函数构建的较低级别 API 提供支持。本指南将首先介绍可用于许多实际场景的高级别 API,接着介绍可为您提供进一步控制和自定义功能的低级别 API。

下面的图表可以帮助您确定要使用哪种 API 来实现您的动画效果。

compass mongodb 中文 compass motion_compass mongodb 中文

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.效果预览如下:

compass mongodb 中文 compass motion_android_02

compass mongodb 中文 compass motion_学习_03

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.效果预览如下:

compass mongodb 中文 compass motion_compass mongodb 中文_04

compass mongodb 中文 compass motion_动画_05

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.效果预览如下:

compass mongodb 中文 compass motion_动画_06


compass mongodb 中文 compass motion_android_07

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.效果预览:

compass mongodb 中文 compass motion_android_08


compass mongodb 中文 compass motion_动画_09

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.效果预览如下:

compass mongodb 中文 compass motion_学习_10

compass mongodb 中文 compass motion_compass mongodb 中文_11

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.效果预览如下:

compass mongodb 中文 compass motion_动画_12


compass mongodb 中文 compass motion_compass mongodb 中文_13

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.完整效果预览:

![在这里插入图片描述](

compass mongodb 中文 compass motion_android_14


compass mongodb 中文 compass motion_Text_15


compass mongodb 中文 compass motion_compass mongodb 中文_16

compass mongodb 中文 compass motion_Text_17

compass mongodb 中文 compass motion_compass mongodb 中文_18

compass mongodb 中文 compass motion_Text_19

17.项目demo源码地址如下:

https://gitee.com/jackning_admin/compose-animation-demo

18.总结:

以上就是今天的内容,可以看出使用compose实现动画很简单方便,效果也还可以,只不过还有一些复杂的动画需要后面慢慢研究,希望compose越来越完善,体检更优,一直往前走.