Android 动画和过渡

356 阅读15分钟

Android 动画和过渡

(一) 动画

JetpackCompose提供了强大且可扩展的API,可以轻松地在应用程序的UI中实现各种动画。下面描述了如何使用这些API以及根据动画场景使用哪些API。
动画在现代移动应用程序中至关重要,以实现流畅和可理解的用户体验。许多Jetpack Compose Animation API都可以作为可组合的函数提供,就像布局和其他UI元素一样,并且它们由使用Kotlin协程暂停函数构建的低级API支持。本指南从在许多实际场景中有用的高级API开始,并继续解释为您提供进一步控制和定制的低级API。
下图可帮助决定用什么API来实现动画。
在这里插入图片描述

(1)如果要设置布局中内容更改的动画:
如果要设置外观和消失的动画:
使用“动画可见性”。
基于状态交换内容:

如果您正在交叉阅读内容:
使用交叉淡入淡出。
否则,请使用AnimatedContent。
否则,请使用Modifier.animateContentSize。
(2)如果动画基于状态:
如果动画发生在合成期间:
如果动画是无限的:
使用rememberInfiniteTransition。
如果同时设置多个值的动画:
使用updateTransition。
否则,请使用animate*AsState。
(3)如果要对动画时间进行精细控制:
使用动画,例如TargetBasedAnimation或DecayAnimation。
**(4)如果动画是真实的唯一来源 ** 使用可设置动画。
(5)否则,请使用AnimationState或animate。

1.高级动画API

Compose为许多应用程序中使用的几种常见动画模式提供了高级动画API。这些API是为符合Material Design Motion的最佳实践而定制的。

1.1动画可见性

AnimatedVisibility可组合设置其内容的外观和消失的动画。
代码示例:

var editable by remember { mutableStateOf(true) }  
AnimatedVisibility(visible = editable) {  
Text(text = “Edit”)  
}  
默认情况下,内容通过淡入和展开来显示,而通过淡出和收缩来消失。可以通过指定EnterTransition和ExitTransition来自定义转换。  
代码示例:  
var visible by remember { mutableStateOf(true) }  
val density = LocalDensity.current  
AnimatedVisibility(  
visible = 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()  
) {  
Text(“Hello”, Modifier.fillMaxWidth().height(200.dp))  
}

1.2为子对象设置进出动画

AnimateVisibility中的内容(直接或间接子对象)可以使用animateEnterExit修改器为每个子对象指定不同的动画行为。每个孩子的视觉效果是在AnimatedVisibility组合中指定的动画和孩子自己的进入和退出动画的组合。

AnimatedVisibility(  
visible = visible,  
enter = fadeIn(),  
exit = fadeOut()  
) {  
// Fade in/out the background and the foreground.  
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {  
Box(  
Modifier  
.align(Alignment.Center)  
.animateEnterExit(  
// Slide in/out the inner box.  
enter = slideInVertically(),  
exit = slideOutVertically()  
)  
.sizeIn(minWidth = 256.dp, minHeight = 64.dp)  
.background(Color.Red)  
) {  
// Content of the notification…  
}  
}  
}  

1.3添加自定义动画

如果要在内置的进入和退出动画之外添加自定义动画效果,要通过AnimatedVisibility的content lambda中的Transition属性访问基础的Transition实例。添加到“过渡”实例的任何动画状态都将与AnimatedVisibility的进入和退出动画同时运行。AnimatedVisibility将一直等到“过渡”中的所有动画都完成后再删除其内容。对于独立于Transition创建的退出动画(例如使用animate_AsState),AnimatedVisibility将无法解释它们,因此可能会在完成之前删除可组合的内容

AnimatedVisibility(  
visible = visible,  
enter = fadeIn(),  
exit = fadeOut()  
) { // this: AnimatedVisibilityScope  
// Use AnimatedVisibilityScope#transition to add a custom animation  
// to the AnimatedVisibility.  
val background by transition.animateColor { state ->  
if (state == EnterExitState.Visible) Color.Blue else Color.Gray  
}  
Box(modifier = Modifier.size(128.dp).background(background))  
}  

1.4 animate_AsState

animate*AsState函数是Compose中用于设置单个值动画的最简单的动画API。只需提供结束值(或目标值),API将从当前值开始动画到指定值

val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)  
Box(  
Modifier.fillMaxSize()  
.graphicsLayer(alpha = alpha)  
.background(Color.Red)  
)  

1.5为儿童设置动画进入/退出

与AnimatedVisibility一样,animateEnterExit修改器在AnimatedContent的内容lambda中可用。使用此选项可以分别将EnterAnimation和ExitAnimation应用于每个直接或间接子对象

1.6添加自定义动画
与AnimatedVisibility一样,转换字段在AnimatedContent的内容lambda中可用。使用此选项可以创建与AnimatedContent过渡同时运行的自定义动画效果。

1.7动画内容大小
animateContentSize修改器可设置大小更改的动画。

var message by remember { mutableStateOf(“Hello”) }  
Box(  
modifier = Modifier.background(Color.Blue).animateContentSize()  
) {  
Text(text = message)  
}  
1.8更新转换  
转换将一个或多个动画作为其子级进行管理,并在多个状态之间同时运行它们。  
状态可以是任何数据类型。  
enum class BoxState {  
Collapsed,  
Expanded  
}  
var currentState by remember { mutableStateOf(BoxState.Collapsed) }  
val transition = updateTransition(currentState)  
val rect by transition.animateRect { state ->  
when (state) {  
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)  
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)  
}  
}  
val borderWidth by transition.animateDp { state ->  
when (state) {  
BoxState.Collapsed -> 1.dp  
BoxState.Expanded -> 0.dp  
}  
}  
enum class DialerState { DialerMinimized, NumberPad }

@Composable  
fun DialerButton(isVisibleTransition: Transition) {  
// `isVisibleTransition` spares the need for the content to know  
// about other DialerStates. Instead, the content can focus on  
// animating the state change between visible and not visible.  
}

@Composable  
fun NumberPad(isVisibleTransition: Transition) {  
// `isVisibleTransition` spares the need for the content to know  
// about other DialerStates. Instead, the content can focus on  
// animating the state change between visible and not visible.  
}

@Composable  
fun Dialer(dialerState: DialerState) {  
val transition = updateTransition(dialerState)  
Box {  
// Creates separate child transitions of Boolean type for NumberPad  
// and DialerButton for any content animation between visible and  
// not visible  
NumberPad(  
transition.createChildTransition {  
it == DialerState.NumberPad  
}  
)  
DialerButton(  
transition.createChildTransition {  
it == DialerState.DialerMinimized  
}  
)  
}  
}

1.9封装转换并使其可重用

对于简单的用例,在与UI相同的可组合中定义转换动画是一个非常有效的选项。然而,当您处理具有多个动画值的复杂组件时,可能需要将动画实现与可组合UI分离。

可以通过创建一个包含所有动画值的类和一个返回该类实例的“update”函数来实现。可以将转换实现提取到新的单独函数中。当需要集中动画逻辑或使复杂动画可重用时,此模式非常有用。

enum class BoxState { Collapsed, Expanded }

@Composable  
fun AnimatingBox(boxState: BoxState) {  
val transitionData = updateTransitionData(boxState)  
// UI tree  
Box(  
modifier = Modifier  
.background(transitionData.color)  
.size(transitionData.size)  
)  
}

// Holds the animation values.  
private class TransitionData(  
color: State,  
size: State  
) {  
val color by color  
val size by size  
}

// Create a Transition and return its animation values.  
@Composable  
private fun updateTransitionData(boxState: BoxState): TransitionData {  
val transition = updateTransition(boxState)  
val color = transition.animateColor { state ->  
when (state) {  
BoxState.Collapsed -> Color.Gray  
BoxState.Expanded -> Color.Red  
}  
}  
val size = transition.animateDp { state ->  
when (state) {  
BoxState.Collapsed -> 64.dp  
BoxState.Expanded -> 128.dp  
}  
}  
return remember(transition) { TransitionData(color, size) }  
}  

2.低级动画API

animate*AsState函数是最简单的API,将即时值更改渲染为动画值。它由Animatable支持,Animatable是一个基于协程的API,用于设置单个值的动画。updateTransition创建一个转换对象,该对象可以管理多个动画值,并根据状态更改运行它们。rememberInfiniteTransition类似,但它创建了一个无限过渡,可以管理无限期运行的多个动画。除了Animatable之外,所有这些API都是可组合的,这意味着这些动画可以在组合之外创建。

所有这些API都基于更基本的动画API。尽管大多数应用程序不会直接与Animation交互,但Animation的一些自定义功能可以通过更高级的API获得。

在这里插入图片描述

2.1可设置动画

Animatable是一个值持有者,可以在通过animateTo更改值时为其设置动画。这是支持animate*AsState实现的API。它确保了一致的延续性和相互排斥性,这意味着价值观的变化总是持续的,任何正在进行的动画都将被取消。
Animatable的许多功能(包括animateTo)都作为挂起函数提供。这意味着需要将它们包装在适当的协程范围中。

// Start out gray and animate to green/red based on `ok`  
val color = remember { Animatable(Color.Gray) }  
LaunchedEffect(ok) {  
color.animateTo(if (ok) Color.Green else Color.Red)  
}  
Box(Modifier.fillMaxSize().background(color.value))  .

2.2动画
动画是可用的最低级别的动画API。到目前为止,我们看到的许多动画都是建立在动画之上的。有两种动画子类型:TargetBasedAnimation和DecayAnimation。

动画只能用于手动控制动画的时间。动画是无状态的,它没有任何生命周期的概念。它充当高级API使用的动画计算引擎。

2.2.1目标基准动画

其他API涵盖了大多数用例,但使用TargetBasedAnimation可以直接控制动画播放时间。在下面的示例中,TargetAnimation的播放时间是根据FrameNanos提供的帧时间手动控制的

val anim = remember {  
TargetBasedAnimation(  
animationSpec = tween(200),  
typeConverter = Float.VectorConverter,  
initialValue = 200f,  
targetValue = 1000f  
)  
}  
var playTime by remember { mutableStateOf(0L) }

LaunchedEffect(anim) {  
val startTime = withFrameNanos { it }


do {
    playTime = withFrameNanos { it } - startTime
    val animationValue = anim.getValueFromNanos(playTime)
} while (someCustomCondition())



}  

2.2.2自定义动画

许多动画API通常接受用于自定义其行为的参数。
动画规格

大多数动画API允许开发人员通过可选的AnimationSpec参数自定义动画规范。

val alpha: Float by animateFloatAsState(  
targetValue = if (enabled) 1f else 0.5f,  
// Configure the animation duration and easing.  
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)  
)  

2.2.3 tween tween使用缓和曲线在指定持续时间Millis内的开始值和结束值之间设置动画

val value by animateFloatAsState(  
targetValue = 1f,  
animationSpec = tween(  
durationMillis = 300,  
delayMillis = 50,

2.2.4关键帧

关键帧基于在动画持续时间的不同时间戳指定的快照值设置动画。在任何给定时间,动画值都将在两个关键帧值之间进行插值。对于这些关键帧中的每一个,以指定Easing来确定插值曲线。

可以选择指定0毫秒和持续时间的值。如果未指定这些值,它们将分别默认为动画的开始值和结束值

val value by animateFloatAsState(  
targetValue = 1f,  
animationSpec = keyframes {  
durationMillis = 375  
0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms  
0.2f at 15 with FastOutLinearInEasing // for 15-75 ms  
0.4f at 75 // ms  
0.4f at 225 // ms  
}  
)

2.2.5infiniteRepeatable  
infiniteRepeatable就像是可重复的,但它会重复无限次数的迭代。  
val value by animateFloatAsState(  
targetValue = 1f,  
animationSpec = infiniteRepeatable(  
animation = tween(durationMillis = 300),  
repeatMode = RepeatMode.Reverse  
)  
) 

(二)过渡动画(Transition)

为布局的变化添加动画效果大概需要以下几个流程:
(1).为起始布局和结束布局个创建一个Scene(场景)对象。然而大部分情况下,我们不需要创建起始布局的Scene对象,因为起始布局的场景通常是根据当前布局自动确定的。
(2).创建一个Transition对象来定义所需的动画类型。
(3).调用TransitionManager.go(),系统会运行动画以交换布局。
部分转载来自:blog.csdn.net/xujian197/a…

1.1创建场景
我们可以直接从布局文件中创建Scene实例。调用Scene.getSceneForLayout()。该方法接受三个参数,第一个参数是场景所在的ViewGroup,第二个参数是场景的布局文件ID,第三个参数是一个Context对象。


<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout  
xmlns:android=“http://schemas.android.com/apk/res/android”  
xmlns:app=“http://schemas.android.com/apk/res-auto”  
android:layout\_width=“match\_parent”  
android:layout\_height=“match\_parent”>  
<androidx.appcompat.widget.Toolbar  
android:id=“@+id/toolbar”  
…>  
</androidx.appcompat.widget.Toolbar>  
  
  
</androidx.constraintlayout.widget.ConstraintLayout>  

2.1.2第一个场景布局文件如下:

<TextView  
android:id=“@+id/text\_view”  
…/>  
<ImageView  
android:id=“@+id/image\_view”  
…/>  
  

2.1.3第二个场景布局文件如下:

<ImageView  
android:id=“@+id/image\_view”  
…/>  
<TextView  
android:id=“@+id/text\_view”  
…/>  

2.1.4两个场景的布局只是交换了两个View的位置(ID相同),下面的代码展示了从布局文件中创建Scene对象:

val sceneRoot: ViewGroup = findViewById(R.id.scene\_root)  
val aScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.a\_scene, mContext)  
val bScene: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.b\_scene, mContext)

2.1.5创建过渡动画类型

过渡动画类型可以从资源文件中指定,也可以从代码中直接指定。如需在资源文件中指定过渡动画类型,可以在res/transition/目录中添加。下面文件指定了一个Fade过渡。res/transition/fade_transition.xml

2.1.6在代码中使用TransitionInflater来加载过渡动画。

ValfadeTransition=TransitionInflater.from(mContext).inflateTransition(R.transition.fade_transition)

2.2开始过渡

2.2.1调用TransitionManager.go()开始过渡动画。该方法接受两个参数,第一个参数是结束布局的场景对象(end scene),第二个为过渡动画类型的实例(transition)。也可以不指定过渡动画类型。框架将使用默认的过渡来执行场景切换时的动画。默认情况下,退出的场景执行退出动画,进入的场景执行进入动画。

2.2.2但有时,我们可能需要在进入或退出场景时做一些额外的操作。比如为不在同一个层次中的视图添加动画效果。这时可以创建一个Runnable对象,并将其传递给Scene.setExitAction()或Scene.setEnterAction()方法。框架会在运行过渡动画之前,在起始场景中调用setExitAction()方法,在运行过渡动画之后,在结束场景中调用setEnterAction()方法。 没有场景的过渡

2.2.3更改视图层次并不是修改界面的唯一方式,还可以通过修改当前视图层次中的子视图来进行更改。如果这种在单个视图层次中来修改界面来应用过渡,可以使用TransitionManager.beginDelayedTransition()创建一个延时过渡。在界面内的子视图更改时,过渡动画框架会在原始状态和新状态的变化添加动画效果。
使用过渡动画为Activity的切换添加动画效果

2.2.4在Android5.0之前,如果我们需要改变默认的Activity切换动画。可能需要在startActivity()和finish()之后,添加一个overridePendingTransition()方法。该方法接受两个参数,第一个参数时进入动画,第二个参数是退出动画。例如,Activity A打开一个Activity B。那第一个参数就是B的进入动画,第二个参数就是A的退出动画。按返回键关闭Activity B,第一个参数就是A的进入动画,第二个参数就是B的退出动画。
或者在主题style文件中分别设置。

2.2.5过渡动画带来了全新的Activity转场动画,这些转场动画可以是针对Activity里面的视图的。可以为进入和退出过渡,以及在Activity之间为共享元素过渡。

  • 1.进入过渡 决定了Activity中的视图如何进入场景
  • 2.退出过渡 决定了Activity中的视图如何退出场景
  • 3.共享元素过渡 决定了两个Activity共享的视图如何在这些Activity之间过渡。

Android支持以下三种进入和退出过渡。

1.分解(Explode),将视图移入到场景中心或从中心移出。
2.滑动(Slide),将视图从场景的其中一个边缘移入或移出。
3.淡入淡出(Fade),通过更改视图的不透明度,在场景中添加或移出视图。

2.2.6Android还支持以下共享元素过渡

1.ChangeBounds 为目标视图布局边界的变化添加动画效果。
2.ChangeClipBounds 为目标视图裁剪边界的变化添加动画效果
3.ChangeTransform 为目标视图缩放和旋转方面的变化添加动画效果
4.ChangeImageTransform 为目标图片尺寸和缩放方面的变化添加动画效果

2.2.7使用代码为Activity的切换指定过渡

Window.setEnterTransition() 指定打开该Activity的进入过渡
Window.setExitTransition() 指定打开某个Activity而使该Activity退出前台的退出过渡
Window.setReenterTransition() 指定关闭某个Activity而使该Activity进入前台的进入过渡
Window.setReturnTransition() 指定关闭该Activity的退出过渡

一般情况下,我们只用设置EnterTransition和ExitTransition就好。ReenterTransition会使用ExitTransition的反向版本,ReturnTransition会使用EnterTransition的反向版本。下面代码为Activity指定了一个从右侧边缘滑入的进入过渡

fun setupWindowAnimations() {  
val slideTransition = Slide().apply {  
slideEdge = Gravity.END  
duration = 300L  
}  
window.enterTransition = slideTransition  
}  

2.2.8或者我们可以在主题style文件中指定Activity的过渡

2.2.9最后,要想使过渡动画起效。必须使用startActivity(Intent intent, Bundle options)启动Activity,关闭Activity也应该调用finishAfterTransition()。Bundle对象可以由ActivityOptionsCompat.makeSceneTransitionAnimation(Activity activity)方法来获取。
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this)
startActivity(intent, options)

2.3共享元素过渡
要开始一个共享元素过渡动画很简单。
1.首先为目标Activity的共享视图添加一个transitionName属性。
2.使用ActivityOptionsCompat.makeSceneTransitionAnimation(Activity activity, View sharedElement, String sharedElementName)方法来获取一个Bundle对象。参数sharedElement代表要过渡到目标活动的视图。sharedElementName表示目标Activity中的共享视图的transitionName属性值。
3.使用startActivity(Intent intent, Bundle options)来启动目标Activity。
3.TransitionManager
此类管理当场景发生变化时激发的一组转换。要使用管理器,请添加场景以及调用setTransition(android.transition.Scene,android.ttransition.transition)或setTransition的转换对象(android.transition.Sene,android.Ttransition.Scene.,android.teransition.Transaction)。不需要为场景更改设置特定的过渡;默认情况下,场景更改将使用“自动转换”来为大多数情况做一些合理的事情。仅当应用程序在这些情况下需要不同的过渡行为时,才需要为特定场景更改指定其他过渡。

TransitionManagers可以在res/transition目录中的XML资源文件中声明。TransitionManager资源由TransitionManager标记名组成,包含一个或多个转换标记,每个转换标记描述该转换与该标记中的从/到场景信息的关系。例如,这里有一个资源文件,它声明了几个场景转换:

对于fromScene和toScene属性中的每一个,都有对标准XML布局文件的引用。这相当于通过调用scene#getSceneForLayout(ViewGroup,int,Context)从代码中的布局创建场景。对于transition属性,在res/transition目录中有一个资源文件的引用,该文件描述了该转换。
3.1Summary
在这里插入图片描述
在这里插入图片描述

3.2Public methods
beginDelayedTransition
open static fun beginDelayedTransition(sceneRoot: ViewGroup!): Unit
方便的方法,使用默认过渡设置动画到由调用此方法和下一个渲染帧之间给定场景根中的所有更改定义的新场景。相当于调用beginDelayedTransition(android.view.ViewGroup,android.transition.transition),transition参数的值为null。
在这里插入图片描述

3.3beginDelayedTransition
open static fun beginDelayedTransition(
sceneRoot: ViewGroup!,
transition: Transition!
): Unit
在这里插入图片描述

关注公众号:Android老皮
解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版

内容如下

1.Android车载应用开发系统学习指南(附项目实战)
2.Android Framework学习指南,助力成为系统级开发高手
3.2023最新Android中高级面试题汇总+解析,告别零offer
4.企业级Android音视频开发学习路线+项目实战(附源码)
5.Android Jetpack从入门到精通,构建高质量UI界面
6.Flutter技术解析与实战,跨平台首要之选
7.Kotlin从入门到实战,全方面提升架构基础
8.高级Android插件化与组件化(含实战教程和源码)
9.Android 性能优化实战+360°全方面性能调优
10.Android零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔