在我们开始之前,值得指出的是,在动画方面很容易超越顶部。虽然很容易在动画中添加越来越多的复杂组件,但有时知道何时停止添加其他组件,甚至删除那些不适合整体流程的组件,是实现感觉自然的动画的关键。此外,最有效的动画通常是那些结合了非常简单的组件动画的动画,这些动画相互补充,并创造出比实际更难实现的动画。
让我们首先看看我们将要实现的整体效果。这是一个工具栏,可能会出现在一个体育应用程序中,该应用程序显示正在进行的足球比赛(美国足球比赛)的信息:
(我应该指出,我对2019年F.A.杯决赛的结果并不感到痛苦。没有丝毫。)
虽然动画中有很多东西在进行,但各种动作都在一起,因此整体效果感觉非常流畅。实际的绿色矩形块Toolbar
实际上并没有改变大小,但是当前的匹配时间文本(89.59
)移动到Toolbar
的边界之外,并且有一种从Toolbar
底部扩展的气泡形状以包含它。这是动画中可能最有趣的部分,因此本文的大部分内容都将关注这一点。
我不打算完整描述MotionLayout
的机制,因为previous article已经涵盖了。关键是我们有效地定义了两个静态,每个静态表示为ConstraintSet
。展开状态如下所示:
崩溃的州看起来像这样:
MotionLayout
本身声明如下:
RES/布局/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/collapsing_toolbar"
tools:context=".MainActivity">
<View
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/colorPrimary" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/event_name"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" />
<TextView
android:id="@+id/score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/score"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" />
<ImageView
android:id="@+id/man_city_logo"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:src="@drawable/man_city" />
<TextView
android:id="@+id/man_city"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/man_city"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
app:layout_constraintBaseline_toBaselineOf="@id/score"
app:layout_constraintEnd_toStartOf="@id/man_city_logo"/>
<ImageView
android:id="@+id/watford_logo"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:src="@drawable/watford" />
<TextView
android:id="@+id/watford"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/watford"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
app:layout_constraintBaseline_toBaselineOf="@id/score"
app:layout_constraintStart_toEndOf="@id/watford_logo"/>
<View
android:id="@+id/toolbar_extension"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bubble"
android:backgroundTint="@color/colorPrimary"
app:layout_constraintEnd_toEndOf="@id/time"
app:layout_constraintStart_toStartOf="@id/time" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/time"
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" />
<FrameLayout
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_extension" />
</androidx.constraintlayout.motion.widget.MotionLayout>
虽然MotionLayout
是ConstraintLayout
的子类,但是在此布局文件中没有声明约束 - 它们都在名为@xml/collapsing_toolbar
的layoutDescriptor
文件中声明。
这个文件包含我们MotionLayout
的MotionScene
:
RES/XML/collapsing_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Transition
app:constraintSetEnd="@id/collapsed"
app:constraintSetStart="@id/expanded">
<OnSwipe
app:dragDirection="dragUp"
app:touchAnchorId="@id/recyclerview"
app:touchAnchorSide="top" />
</Transition>
<ConstraintSet android:id="@+id/collapsed">
...
</ConstraintSet>
<ConstraintSet android:id="@+id/expanded">
...
</ConstraintSet>
</MotionScene>
这将在其自己的ConstraintSet
中声明扩展和折叠的约束,并将其关联为拖动手势,以便拖动两个状态之间的过渡。 对于不熟悉这一点的人:earlierMotionLayout
article更详细地介绍了这一点。
我们应用于各个视图的大多数动画都在缩放它们。 如果你看看早期的动画GIF显示动画的外观,并专注于顶部的文本F.A. Cup Final 2019
,它只是变得越来越小,ConstraintSet
s中的约束是:
RES/XML/collapsing_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
...
<ConstraintSet android:id="@+id/collapsed">
...
<Constraint android:id="@id/title">
<Transform
android:scaleX="0.5"
android:scaleY="0.5" />
<Layout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/score"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/toolbar" />
</Constraint>
...
</ConstraintSet>
<ConstraintSet android:id="@+id/expanded">
...
<Constraint android:id="@id/title">
<Transform
android:scaleX="1.0"
android:scaleY="1.0" />
<Layout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
...
</ConstraintSet>
</MotionScene>
尽管layout_constraint*
属性存在一些细微差别,但这里的关键是Transform
,它将在collpased和expanded状态之间缩放文本。 MotionLayout
将自动为我们进行这种缩放。 我们免费获得的是,如果我们将其他View
限制在View
的底部,他们将随着缩放应用于此View
而移动。 所以视觉效果是它下面的View
随着它的增长和收缩而移动。 在GIF中查看团队名称和分数如何随着标题文本的大小变化而移动。
我们使用相同的技术来缩放团队徽标和匹配时间文本(89:59
)。 我不会单独介绍这些内容,但请查看accompanying source code以查看此内容。
但是匹配时间文本值得一看:
RES/XML/collapsing_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
...
<ConstraintSet android:id="@+id/collapsed">
...
<Constraint android:id="@id/toolbar">
<Layout
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
...
<Constraint android:id="@id/time">
<Transform
android:scaleX="0.5"
android:scaleY="0.5" />
<Layout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/toolbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/score" />
</Constraint>
...
</ConstraintSet>
<ConstraintSet android:id="@+id/expanded">
...
<Constraint android:id="@id/toolbar">
<Layout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/time"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</Constraint>
...
<Constraint android:id="@id/time">
<Transform
android:scaleX="1.0"
android:scaleY="1.0" />
<Layout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/score"
android:layout_marginTop="8dp"/>
</Constraint>
...
</ConstraintSet>
</MotionScene>
正在进行相同类型的缩放,但在折叠状态下它位于Toolbar
内部,但在展开状态下它位于其下方。
就其自身而言,这对于扩展状态来说效果不佳,因为文本本身很轻,而Toolbar
下面的背景也很轻。 因此,我们需要将绿色“泡沫"降至Toolbar
以下,以确保文本具有对比鲜明的背景。
泡沫本身就是一个VectorDrawable
:
RES/抽拉/bubble.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="16dp"
android:viewportWidth="150"
android:viewportHeight="50">
<path
android:fillColor="@android:color/white"
android:pathData="M0,0 a 25,25 0 0 1 25,25 a 25,25 0 0 0 25,25 h 50 a 25,25 0 0 0 25,-25 a 25,25 0 0 1 25,-25 Z" />
</vector>
它由一些弧线和水平线组成,实际上看起来像这样:
形状应该在我们之前看到的静态展开状态图像中可见,并且在布局中应用绿色。 它变得有趣的地方是如何在MotionScene
中应用它:
RES/XML/collapsing_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
...
<ConstraintSet android:id="@+id/collapsed">
...
<Constraint android:id="@id/toolbar_extension">
<Layout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/toolbar"
app:layout_constraintEnd_toEndOf="@id/time"
app:layout_constraintStart_toStartOf="@id/time"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
</Constraint>
...
</ConstraintSet>
<ConstraintSet android:id="@+id/expanded">
...
<Constraint android:id="@id/toolbar_extension">
<Layout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/time"
app:layout_constraintEnd_toEndOf="@id/time"
app:layout_constraintStart_toStartOf="@id/time"
app:layout_constraintTop_toTopOf="@id/time" />
</Constraint>
...
</ConstraintSet>
</MotionScene>
在折叠状态下,此View
的顶部和底部都被约束到Toolbar
的底部。结果没有高度。
在展开状态下,此View
的顶部和底部都被约束到匹配时间TextView
的顶部和底部。它的高度与匹配时间TextView
相匹配。
当MotionLayout
在折叠状态和展开状态之间转换时,气泡的高度将发生变化,这会产生从Toolbar
底部生长的气泡的错觉。我们实际上可以得到一个微妙的不同效果,我们保持泡沫与匹配时间TextView
保持一致。但我个人更喜欢这种效果,因为它会使气泡看起来增长而不是从Toolbar
中滑出 - 我觉得如果感觉更有机。
我们完成了。如果我们把所有这些放在一起,我们得到以下结果:
这种特定的实现可能不适合某些情况,因为匹配时间将覆盖Toolbar
下面的任何位置,但我认为形状改变工具栏扩展是一个有趣的想法,这就是我在这里分享它的原因。
最后
如果你看到了这里,觉得文章写得不错就给个赞呗!欢迎大家评论讨论!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足,定期免费分享技术干货。谢谢!