使用运动布局(和协同程序)探索复杂的多步动画。
https://thumbs.gfycat.com/HairyWellwornGelding-mobile.mp4
运动布局是在动画、过渡、复杂动作以及你拥有的功能上的新助手。本文中,我们将研究运动布局和Coroutines是如何帮助我们构建多步动画的。
上一篇文章深入探讨了没有使用运动布局的不同动画和小部件,我希望在阅读本文之前你能看看之前的文章,因为:
1.在本文中,我们将只讨论过滤器工作表转换,而不会讨论适配器、选项卡和其他动画。
2.你会理解和欣赏使用运动布局编写这些动画和不使用时的区别。
Android上复杂的UI/动画
如何在Adnroid上编写复杂的多步动画.
proandroiddev.com
什么是运动布局?快速介绍…
简而言之,
运动布局
是一个允许你可以轻松在两个ConstraintSet之间进行转换的
ConstraintLayout
。
<ConstraintSet>
包含每个视图的所有约束和布局属性。
<Transition>
指定要在其之间进行过渡的起始ConstraintSets。
将所有这些都放入
<MotionScene>
文件中,你就可以 拥有一个运动布局啦!
随着布局和动画变得越来越复杂,MotionScene也变得越来越复杂,接下来我们将看一下这些组件。
了解有关运动布局的更多信息:
#1 Nicolas Roard的 运动布局 系列 简介 。
#2 James Pearson的高级和实用的 运动布局 演讲。
所有动画放在一起,该项目的运动场景文件包含 10个 约束集 和 9个 过渡 。以下视频演示了所有的ConstraintSets和Transitions,我们将寻找到 4个动画 :
打开过滤器表:Set1→Set2→Set3→Set4 关闭滤纸:
Set4→Set3→Set2→Set1 应用过滤器:
Set4→Set5→Set6→Set7 删除过滤器:
Set7→Set8→Set9→Set10
注意: 背景中的RecyclerView Items动画不是 运动布局 的一部分。在本文的后面,我们将看到如何 使用 运动布局 编排外部动画。
本文中的每个动画(GIF)都会在其下方显示约束集详细信息(Ex: Set 4, Transitioning…, Set 5, etc),以便在阅读和浏览源代码时更易于操作。
约束集是运动布局执行动画所需的 构造块 。你可以在此处指定所有约束、布局属性等。
一个
<ConstraintSet>
必须包含一个
<Constraint>
元素,并且该元素带有每个你想要将场景动画化的布局属性
分解你的元素
你可以在
<Constraint>
元素中指定所有布局属性。
但是
对于更复杂的动画,你应该使用
<Layout> <PropertySet> <Transform> <Motion> <CustomAttribute>
标签对其进行分解。
这允许你仅覆盖所需的属性,而无需重复写下所有属性。
1_P5OuFMZvsxccOKl5lgSqQg.png 1250×384 174 KBapp:deriveConstraintsFrom =“…”
deriveConstraintsFrom
是一个非常有用的标签,它允许你继承其他的
<ConstraintSet>
。这样,你不必重写所有视图/约束/属性,而只需重写要设置动画的视图/约束/属性就可以了。
将其与之前分解的
<Constraint>
元素结合起来,你将获得包含你想更改部分的简洁版约束集。
在该项目中,10个约束集中的每一个都从先前的集合派生而来,并且仅修改需要动画的内容。例如:在以下转换中,关闭图标旋转是通过从
Set5
中导出所有约束并仅在
Set6
中应用旋转来完成的。
警告:覆盖 其中一个元素时,该元素中的所有属性都会被覆盖,因此你必须从该元素复制其他属性。
必要时展开你的视图
运动布局只能与它直接初始视图一起使用,并且没有嵌套视图。
例如,在此动画中,过滤器图标看起来像是圆形FAB(
CardView
)的一部分,但是它们被分成不同的视图,因为在动画中它们各自都有自己的工作。
同样,fab的高程从
Set1 → Set2
设置为动画,图标必须放在更高的位置才能显示。图标的这种不理想的效果是投射了自己的阴影。为了防止这种情况,我们可以使用:
android:outlineProvider="none"
阴影是由视图轮廓提供者创建的。如果将其设置为 none
,则不会创建阴影。
自定义属性
运动布局提供了我们要设置动画的大多数基本属性,但是它不能提供 一切 。例如,自定义视图可能需要设置其他属性的动画。
< CustomAttribute >通过允许你在视图中使用任何 设置器 来弥合这种差距。它使用反射来调用方法并设置值。
<CustomAttribute
app:attributeName =“ radius”
app:customDimension =“ 16dp” />
注意:你必须使用设置器名称,而不是xml attr名称。例如,CardView有一个 setRadius()
法,而xml中的方法是 app:cardCornerRadius
。CustomAttribute应该引用设置器-“ radius”。
“隐形” vs“消失”
设置从 invisible
/ gone
到 visible
的可见性动画时,需要留意这种差异。
✓ gone → visible
将设置 Alpha 动画 并缩放 。
✓ invisible → visible
将 仅对alpha进行 动画处理。
过渡是2个约束集之间的连接,它们指定了在其之间进行 转换 的开始和结束状态。
<Transition
app:constraintSetStart =“ @ id / set1”
app:constraintSetEnd =“ @ id / set2”
app:motionInterpolator =“ linear”
app:duration =“ 300” />
你还可以使用 <OnClick>
和 <OnSwipe>
元素在过渡中指定滑动和单击相关的功能,但是由于我们正在观察的10套动画不太需要 , 所以 我们将不在本文 中介绍。
我们可以使用 app:motionInterpolator
来为转换指定插值器。可用选项为 linear
、 easeIn
、 easeOut
和 easeInOut
。当你把它们比作 AnticipateInterpolator
、 BounceInterpolator
等,那是不够的。
https://cubic-bezier.com/#0,1,.5,1
对于这些情况,你可以使用 cubic()
选项,在其中可以使用 贝塞尔曲线 定义自己的插值器。你可以创建自己的贝塞尔曲线,并在cube-bezier.com上获取值。
可以使用以下方法进行设置:
`app:motionInterpolator=”cubic(0,1,0.5,1)`
有时,仅具有开始和结束状态是不够的。对于更复杂的动画,我们可能希望更详细地指定过渡的过程。关键帧可帮助我们在过渡中指定 “检查点”, 在该过渡中我们可以在任何给定时间更改视图的任一属性。
文章“在运动布局中定义运动路径”深入探讨了关键帧及其使用方法。
左 :有关键帧… 右 :无关键帧
左侧的动画具有 9个关键帧 ,而右侧的动画则没有关键帧。
如你所见,它们的开始(设置4)和结束(设置5)相同。但是,通过使用关键帧,我们可以更好地控制每个元素在过渡期间发生的变化。
结构化关键帧
每个 <Transition />
都可以具有一个或多个 <KeyFrameSet />
已经指定的所有关键帧的元素。对于此项目,仅用了 <KeyPosition />
和 <KeyAttribute />
元素。
motionTarget
指定哪个视图受关键帧影响。
framePosition
指定在过渡期间何时应用关键帧(0-100)
<KeyPosition />
用于指定宽度、高度和x,y坐标的变化
<KeyAttribute />
用于指定其他任何改动, 包括 CustomAttributes 。
framePosition = 0 vs 1
有时,我们想在动画的一 开始 就更改属性。在普通动画中,可以通过使用 animator.doOnStart{...}
或类似方法实现。让我们尝试通过关键帧实现相同的效果。
左 :framePosition = 1… 右 :framePosition = 0
在此特定动画中,当用户单击“过滤器”按钮时,动画首先将fab(CardView)更改为一个圆形并缩小其大小。
这里的问题是,在动画开始时,何时将 framePosition = 0
用来更改值,运动布局不会记录它。
因此,如果你希望关键帧在任何过渡开始时都指定一些内容,请改用 framePosition = 1
。
<KeyAttribute
app:motionTarget =“ @ id / fab”
app:framePosition =“ 1”>
<CustomAttribute
app:attributeName =“ radius”
app:customDimension =“ 600dp” />
</ KeyAttribute>
必要时使用自定义视图
CustomAttributes
的可用性允许我们使用自定义视图灵活的进行布局。
例如,此动画中的许多过渡都涉及到FAB( CardView
)来扩大和缩小 为一个圆形 。问题是,要将CardView保持为圆形 cornerRadius must be <= size/2
。通常情况下,使用类似 ValueAnimator
的方 法很容易,因为我们一直都知道所有值。
但是, 运动布局
使所有计算都远离了我们。因此,要实现这一点,我们必须引入一个新的视图:
CircleCardView
通过将半径限制为最大size / 2来处理这种情况。现在,当 运动布局
调用设置器时(还记得 CustomAttributes
吗?),我们就不会遇到问题了。
编排多步动画
当前,运动布局没有允许受控的多步过渡API。我们可以使用 autoTransition
,但是有很大的局限性(我们将在后面讨论)。在伪代码中,以下是你要执行的操作:
// Transitioning from set1 -> set2 -> set3 -> set4
motionLayout.setTransition(set1, set2)
motionLayout.transitionToEnd()
motionLayout.doOnEnd {
motionLayout.setTransition(set2, set3)
motionLayout.transitionToEnd()
motionLayout.doOnEnd {
motionLayout.setTransition(set3, set4)
motionLayout.transitionToEnd()
motionLayout.doOnEnd {
这就很恶心了,又变成了可怕的回调地狱。 另一方面 , 协程可以 帮助我们将异步回调代码转换为线性代码。
运动布局.awaitTransitionComplete()
克里斯·班纳斯(Chris Banes)撰写的有关“在视图上暂停”的文章是有关如何在与视图相关的代码中实现协程的必读文章。
暂停视图 — 示例
A worked example from the Tivi app
medium.com
他向我们介绍了 awaitTransitionComplete()
,这是一个 暂挂函数 ,可隐藏所有侦听器,使你可以轻松地使用协程完成转换:
1_yFk2YDqAfkC_pCugcEg5Og.jpg1250×431 162 KB
注意:所述的 awaitTransitionComplete()
扩展方法使用修饰 运动布局
,它能设置多个侦听器而不是只有一个(要被设置功能的请求)。
autoTransition
是在 没有协程时 实现多步过渡的最简单方法。假设我们要从 Set7 → Set8 → Set9 → Set10
实现 “ Removing Filters” 动画。
现在,如果这样做 运动布局
.transitionToState(set8)
,运动布局从 Set7 → Set8
开始过渡,到达 Set8
时,它会 自动转换 为 Set9
,并与 Set10
类似。
当运动布局在 constraintSetStart
中达到指定的ConstraintSet时,autoTransition
将自动执行过渡。
自动转换并不完美
如果再次观看动画,你会注意到有一个动画正在进行,适配器项在后台。为了与运动布局转换 并行 完成这些动画,我们必须使用协程,仅使用 autoTransition
不能正确设置它们的时间。
private fun unFilterAdapterItems(): Unit = lifecycleScope.launch {
// 1) Set7 -> Set8 (Start scale down animation simultaneously)
motionLayout.transitionToState(R.id.set8)
startScaleDownAnimator(true) // Simulataneous
motionLayout.awaitTransitionComplete(R.id.set8)
// 2) Set8 -> Set9 (Un-filter adapter items simultaneously)
(context as MainActivity).isAdapterFiltered = false // Simulataneous
motionLayout.awaitTransitionComplete(R.id.set9)
// 3) Set9 -> Set10 (Start scale 'up' animation simultaneously)
startScaleDownAnimator(false) // Simulataneous
motionLayout.awaitTransitionComplete(R.id.set10)
标有 //Simultaneous
的线与正在发生的过渡同时发生。
由于 autoTransition
不能从一个过渡到下一个,因此 awaitTransitionComplete()
仅在过渡完成时通知我们。它实际上 并不会 等到转换的结束。这就是为什么我们一开始只使用 transitionToState()
一次的原因。
多步前进和后退过渡
自动转换与协同程序相结合可帮助我们实现对多步转换的控制。
但是,如果我们想在每次过渡时后退 Set4 → Set1
,该怎么办?
例如,反转 特定的 过渡。使用 transitionToStart()
完成 Set4 → Set3
。如果我们使用 autoTransition
,那么就会因为autoTransition而自动动画到 Set3
,然后再自动回到 Set4
。
由于未使用 autoTransition
,因此打开过滤器表的代码与上一节中看到的代码略有不同。
/** Order of animation: Set1 -> Set2 -> Set3 -> Set4 */
private fun openSheet(): Unit = lifecycleScope.launch {
// Set the start transition. This is necessary because the
// un-filtering animation ends with set10 and we need to
// reset it here when opening the sheet the next time
motionLayout.setTransition(R.id.set1, R.id.set2)
// 1) Set1 -> Set2 (Start scale down animation simultaneously)
motionLayout.transitionToState(R.id.set2)
startScaleDownAnimator(true) // Simultaneous
motionLayout.awaitTransitionComplete(R.id.set2)
// 2) Set2 -> Set3
motionLayout.transitionToState(R.id.set3)
motionLayout.awaitTransitionComplete(R.id.set3)
// 3) Set3 -> Set4
motionLayout.transitionToState(R.id.set4)
motionLayout.awaitTransitionComplete(R.id.set4)
每次等待后我们都必须使用 transitionToState()
。之前没有必要这样做,因为 autoTransition
无需等待就可以经过所有对象。而在这里,我们必须手动进行。
注意,等待之后我们不会每次都使用 setTransition()
。这是因为 运动布局
将根据 transitionToState()
中提到的当前约束集和ConstraintSet来标识要使用的转换。
收盘动画(反向)
/** Order of animation: Set4 -> Set3 -> Set2 -> Set1 */
private fun closeSheet(): Unit = lifecycleScope.launch {
// We don't have to setTransition() here since current transition is Set3 -> Set4.
// transitionToStart() will automatically go from:
// 1) Set4 -> Set3
motionLayout.transitionToStart()
motionLayout.awaitTransitionComplete(R.id.set3)
// 2) Set3 -> Set2
motionLayout.setTransition(R.id.set2, R.id.set3)
motionLayout.progress = 1f
motionLayout.transitionToStart()
motionLayout.awaitTransitionComplete(R.id.set2)
// 3) Set2 -> Set1 (Start scale 'up' animator simultaneously)
motionLayout.setTransition(R.id.set1, R.id.set2)
motionLayout.progress = 1f
motionLayout.transitionToStart()
startScaleDownAnimator(false) // Simultaneous
motionLayout.awaitTransitionComplete(R.id.set1)
由于所有 <Transition>
元素都是基于正向的,因此我们必须添加几行代码使其可逆。它的本质是:
// Set the transition to be reversed (MotionLayout can only detect forward transitions).
motionLayout.setTransition(startSet, endSet)
// This will set the progress of the transition to the end
motionLayout.progress = 1f
// Reverse the transition from end to start
motionLayout.transitionToStart()
// Wait for transition to reach the start
motionLayout.awaitTransitionComplete(startSet)
// Repeat for every transition...
现在,这使我们能够反向完成多个转换,同时保持并执行其他操作的能力。
结论—是否使用运动布局?
运动布局
与协程结合使用,可以非常轻松地用很少的代码来实现非常复杂的动画, 同时保持平面视图层次!
运动布局十分简洁,并为我们提供了很多有用的东西。使用协程和即将推出的IDE编辑器,其前途无可限量。
希望你会喜欢这篇文章!感兴趣的话,请查看源代码!!
原文作者 Nikhil Panju
原文链接 https://proandroiddev.com/complex-ui-animations-on-android-featuring-motionlayout-aa82d83b8660