Jetpack Compose 动画 API: AnimatedVisibility AnimatedContent

news/2024/8/26 7:29:00

Jetpack Compose 的动画相关的 API 数量众多,分为低级别 API 和高级别 API,其中高级别 API 便于使用者针对具体场景开箱即用 ,其中最常用的当属 AnimatedVisibilityAnimatedContent 这两个了。


1. AnimatedVisibility


AnimatedVisibility 顾名思义是用动画的方式改变 UI 元素的 Visibility,具体来说就是针对让其内部的 Composable 以动画的形式进入或退出屏幕

AnimatedVisibility 可以接受一个 visible 的 boolean 参数,控制内部元素的显示或隐藏

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilitySample() {
    var editable by remember { mutableStateOf(true) }

    Column(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "AnimatedVisibility",
            style = MaterialTheme.typography.h6
        )

        AnimatedVisibility(visible = editable) {
            Surface(
                color = Color.Yellow,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .align(Alignment.CenterHorizontally)
                    .padding(8.dp)
            ) {}
        }

        Button(
            onClick = { editable = !editable },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Toggle")
        }
    }
}

看上面的例子,点击 Button 时,editable 发生变化, AnimatedVisibility 内的 Surface 显示或隐藏,同时伴随动画,效果如下:
请添加图片描述

MutableTransitionState

AnimatedVisibility 还有一个重载的方法, 接收一个 MutableTransitionState 类型的 visibleState 参数,上面的代码还可以写成下面这样

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityStateSample() {
    val state = remember {
        MutableTransitionState(false).apply {
            targetState = true
        }
    }

    Column(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "AnimatedVisibilityState",
            style = MaterialTheme.typography.h6
        )

        AnimatedVisibility(visibleState = state) {
            Surface(
                color = Color.Yellow,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .align(Alignment.CenterHorizontally)
                    .padding(8.dp)
            ) {
                Text(
                    text = state.getAnimationState().toString()
                )
            }
        }

        Button(
            onClick = { state.targetState = !state.currentState },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Toggle")
        }
    }
}

MutableTransitionState 的定义如下,主要有 currentState 和 targetState 两个成员组成,

class MutableTransitionState<S>(initialState: S) {

    var currentState: S by mutableStateOf(initialState)
        internal set

    var targetState: S by mutableStateOf(initialState)

    val isIdle: Boolean
        get() = (currentState == targetState) && !isRunning

    internal var isRunning: Boolean by mutableStateOf(false)
}

前面的例子中,MutableTransitionState 的初始状态 currentState 为 false,目标状态 targetState 为 true,状态差可以实现 AnimatedVisibility 上屏时立即执行动画的效果。
请添加图片描述

MutableTransitionState 的 currentStateisIdle 可以暴露当前动画的执行状态给外面参考,我们可以定义一个枚举表示动画的状态

enum class AnimState {
    VISIBLE,
    INVISIBLE,
    APPEARING,
    DISAPPEARING
}

fun MutableTransitionState<Boolean>.getAnimationState(): AnimState {
    return when {
        this.isIdle && this.currentState -> AnimState.VISIBLE
        !this.isIdle && this.currentState -> AnimState.DISAPPEARING
        this.isIdle && !this.currentState -> AnimState.INVISIBLE
        else -> AnimState.APPEARING
    }
}

EnterTransition & ExitTransition

AnimatedVisibility 可以通过 enterexit 参数指定动画样式,enter 和 exit 分别制定一个 EnterTransition 和一个 ExitTransition 。

例如我们可以指定 fadeIn 和 fadeOut 的动画效果:

@ExperimentalAnimationApi
@Composable
fun AnimatedVisibilityEnterExitSample() {
    val state = remember {
        MutableTransitionState(false).apply {
            targetState = true
        }
    }

    Column(modifier = Modifier.padding(8.dp)) {
        Text(
            text = "AnimatedVisibilityState",
            style = MaterialTheme.typography.h6
        )

        AnimatedVisibility(
            visibleState = state,
            enter = fadeIn(),
            exit = fadeOut()
        ) {
            Surface(
                color = Color.Yellow,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .align(Alignment.CenterHorizontally)
                    .padding(8.dp)
            ) {
                Text(text = state.getAnimationState().toString())
            }
        }

        Button(
            onClick = { state.targetState = !state.currentState },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Toggle")
        }
    }
}

请添加图片描述

2. AnimatedContent


AnimatedContent 和 AnimatedVisibility 类似,都是通过动画完成 content 内部的状态变化,AnimatedVisibility 是控制显隐,AnimatedContent 是控制切换:

@ExperimentalAnimationApi
@Composable
fun AnimatedContentCounterDefault() {
    Column {
        var count: Int by remember { mutableStateOf(0) }

        AnimatedContent(targetState = count) { targetCount ->
            Text(
                text = "$targetCount",
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h2,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(onClick = { count++ }) {
                Text("PLUS")
            }

            Button(onClick = { count-- }) {
                Text("MINUS")
            }
        }
    }
}

如上,AnimatedContent 接收一个 targetState 参数,同时 content 基于 targetContent 构建新的 UI,targetState 的不同导致 content 的不同,当 targetState 发生变化时,content 在动画中完成切换。
targetState 可以是任意类型的值,上面例子中,我们基于 Int 型的 count 生成新的 content,点击 PLUS 和 MINUS 之后,content 的 Text 在动画中完成切换:请添加图片描述

ContentTransform

切换动画可以通过 transitionSpec 参数设置一个 ContentTransform, ContentTransform 可以通过 with 中缀操作符,组合 EnterTransition 和 ExitTransition 而成。

ContentTransform = EnterTransition with ExitTransition

  • EnterTransition:新的 content 的进入时的动画
  • ExitTransition: 旧的 content 退出时的动画

例如,我们使用 ContentTransform 实现一个 Slide 效果的切换动画:

  • 从右到左的切换,并伴随淡入淡出效果:
    • EnterTransion:使用 slideInHorizontally,初始位置 initialOffsetX = width
    • ExitTransition:使用 slideOutHorizontally,目标位置 targetOffsetX = -width
slideInHorizontally({ width -> width }) + fadeIn() 
 with slideOutHorizontally({ width -> -width }) + fadeOut()
  • 从左到右的切换,并伴随淡入淡出效果:
    • EnterTransion:使用 slideInHorizontally,初始位置 initialOffsetX = -width
    • ExitTransition:使用 slideOutHorizontally,目标位置 targetOffsetX = width
slideInHorizontally({ width -> -width }) + fadeIn()
 with slideOutHorizontally({ width -> width }) + fadeOut()

我们应用上述 transitionSpec 后的代码:

@ExperimentalAnimationApi
@Composable
fun AnimatedContentCounterCustom() {
    Column {
        var count: Int by remember { mutableStateOf(0) }

        AnimatedContent(
            targetState = count,
            transitionSpec = {
                val isPlus = targetState > initialState
                if (isPlus) {
                    slideInHorizontally({ width -> width }) + fadeIn() with slideOutHorizontally({ width -> -width }) + fadeOut()
                } else {
                    slideInHorizontally({ width -> -width }) + fadeIn() with slideOutHorizontally({ width -> width }) + fadeOut()
                }.using(
                    SizeTransform(clip = false)
                )
            }
        ) { targetCount ->
            Text(
                text = "$targetCount",
                textAlign = TextAlign.Center,
                style = MaterialTheme.typography.h2,
                modifier = Modifier.fillMaxWidth()
            )
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
            Button(onClick = { count++ }) {
                Text("PLUS")
            }

            Button(onClick = { count-- }) {
                Text("MINUS")
            }
        }
    }
}

点击 PLUS 时,数字从左到右移入,点击 MINUS 时,数字从右到左移出,效果如下:
请添加图片描述

SizeTransform

transitionSpec 中指定 ContentTransform 的同时,还可以通过 using 中缀添加 SizeTransform 动画

SizeTransform = EnterTransition with ExitTransition using SizeTransform

SizeTransform 中或获取旧 content 和新 content 的 size,并通过 keyframes 定义动画的执行规则:在何时应该多大且总的持续时长是多少

fadeIn() with fadeOut() using SizeTransform { initialSize, targetSize ->
	keyframes {
		// at :在 250 时刻应有的大小
		IntSize(initialSize.width, initialSize.height) at 250

		// durationMillis :动画的执行总时间
		durationMillis = 500
	}
}

添加 SizeTransform 之后的全部代码如下:

@ExperimentalMaterialApi
@ExperimentalAnimationApi
@Composable
fun AnimatedContentExpandableTextSample() {
    var expanded by remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.primary,
        onClick = { expanded = !expanded },
        modifier = Modifier.padding(8.dp)
    ) {
        AnimatedContent(
            targetState = expanded,
            transitionSpec = {
                fadeIn() with fadeOut() using SizeTransform { initialSize, targetSize ->
                    keyframes {
                        IntSize(initialSize.width, initialSize.height) at 250
                        durationMillis = 500
                    }
                }
            }
        ) { targetExpanded ->
            if (targetExpanded) {
                Text(
                    text = "Expanded",
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(100.dp)
                )
            } else {
                Text(
                    text = "Not Expanded",
                    modifier = Modifier.wrapContentSize()
                )
            }
        }
    }
}

添加 SizeTransform 之后,点击按钮后的变化过程中,content 面积也会出现过度动画,效果如下:
请添加图片描述


3. 总结一下


AnimatedVisibility 和 AnimatedContent 是最常用的 Composable 动画 API ,它们向其他 Layout 元素一样都是 Composable 函数,动画效果作用于其内部的子 Composable:

  • AnimatedVisibility 用来实现 content 的动画显隐,通过 enter 和 exit 可以自定义动画效果
  • AnimatedContent 用来实现 content 的动画切换,等价于旧 content 的隐藏 + 新 content 的显示 ,通过 ContentTransform 和 SizeTransform 可以自定义动画效果
  • 需要注意 AnimatedVisibility 和 AnimatedContent 仍然处于 @Experimental 状态,stable 之前也许会有所变化。

http://www.niftyadmin.cn/n/2071147.html

相关文章

网友提议 Kotlin 增加这些新特性。。

前言 Kotlin 是一门开放的语言&#xff0c;不仅仅是源码的开放&#xff0c;任意使用者都可以直接参与它的建设。大家可以通过 YouTrack 向社区提出自己的 idea 和 issue &#xff0c;其中一些呼声高的 issue 会进入 KEEP 交由 Kotlin 团队管理维护&#xff0c;并有可能被最终实…

Jetpack Hilt 的 @EnterPoint 注解使用介绍

Hilt 是 Android Jetpack 中的依赖注入框架。依赖注入是构建大型项目必不可少的技术手段&#xff0c;通过依赖注入我们解耦了对象的生产与消费&#xff0c;实现了关注点分离的设计目标&#xff0c;同时也方便单元测试。 Hilt 继承了 Dagger 编译期检查等优秀特性&#xff0c;通…

为什么 RxJava 有 Single / Maybe 等单发数据类型,而 Flow 没有?

Coroutine Flow 与 RxJava 都是流式数据处理框架&#xff0c; 所以经常被拿来做比较&#xff0c;本文比较一下他们的数据类型。 Rx 与 Flow 在单发数据支持上的不同 RxJava 支持多种数据类型 Observable &#xff1a;流式数据类型Flowable&#xff1a;与 Observable 类似&am…

Java延迟加载建议

http://blog.csdn.net/tkd03072010/article/details/7793110转载于:https://www.cnblogs.com/xtdxs/p/6673643.html

像 Compose 那样写代码 :Kotlin DSL 原理与实战

1. 前言 Kotlin 是一门对 DSL 友好的语言&#xff0c;它的许多语法特性有助于 DSL 的打造&#xff0c;提升特定场景下代码的可读性和安全性。本文将带你了解 Kotlin DSL 的一般实现步骤&#xff0c;以及如何通过 DslMarker &#xff0c; Context Receivers 等特性提升 DSL 的易…

创建 ViewModel 的新方式,CreationExtras 了解一下?

Androidx-Lifecycle 在近期迈入到了 2.5.0 版本&#xff0c;其中最重要的一个变化是引入了 CreatioinExtras 的概念。一句话概括 CreationExtras 的作用&#xff1a;帮助我们在创建 ViewModel 时更优雅地获取初始化参数 1. 现状的问题 先回顾一下目前为止的 ViewModel 的创建方…

巧用 @JvmName 解决 Kotlin 函数签名冲突

Kotlin(JVM) 中定义下面这样两个方函数时&#xff0c;编译器会报错 fun foo(value: List<String>) {}fun foo(value: List<Int>) {}Platform declaration clash: The following declarations have the same JVM signature (method(Ljava/util/List;)V):因为 Java …

Jetpack MVVM 七宗罪之六: ViewModel 的接口暴露不合理

在 Jetpack 架构规范中&#xff0c; ViewModel 与 View 之间应该遵循单向数据流的通信方式&#xff0c;Events 永远从 View 流向 VM &#xff0c;而 State 从 VM 流向 View。 如果 ViewModel 对 View 暴露了不适当的接口类型&#xff0c;则会破坏单向数据流的形成。不适当的接口…