在Google IO 2018上宣布了ConstraintLayout 2.0,最大的新增功能是MotionLayout,它为我们提供了一个用于布局动画的惊人新工具。Nicolas Roard已经发表了对MotionLayout的精彩介绍,我强烈建议您阅读一下,以了解MotionLayout的基本知识和组件。在这个简短的系列中,我们将看看如何使用MotionLayout创建一个我们都应该熟悉的行为:折叠工具栏。
在开始之前,值得一提的是在CoordinatorLayout中使用CollapsingToolbarLayout来实现此行为并没有任何错误。此外,如果您已经在应用程序中工作,那么通过更改几乎无法获得。也就是说,哪个CoordinatorLayout提供了一些非常有用的行为,试图调整它们甚至创建自己的自定义行为是相当困难的。正是在这种情况下,MotionLayout可以提供更大的灵活性,而且我的早期实验表明,实现您想要的更容易。此外,MotionLayout开辟了一些非常难以实现的新行为CoordinatorLayout。
MotionLayout与Android上许多其他动画框架之间的主要区别之一是视图动画和属性动画在给定的持续时间内运行。虽然可以指定持续时间并取消正在运行的动画,但是无法根据用户输入控制正在运行的动画。例如,折叠工具栏应根据用户滚动进行展开和折叠,实际动画应遵循用户的拖动。这些框架根本不可能实现这一点。
让我们首先看看我们试图模仿的行为。这是一个折叠工具栏,它使用材料组件库和CoordinatorLayout中的CollapsingToolbarLayout实现:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="200dp"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleGravity="bottom"
app:expandedTitleMarginEnd="@dimen/activity_horizontal_margin"
app:expandedTitleMarginStart="@dimen/activity_horizontal_margin"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:title="@string/app_name">
<ImageView
android:id="@+id/toolbar_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
android:src="@drawable/beach_huts" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/ThemeOverlay.AppCompat" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
我们从中获得的行为是这样的:
traditional.gif
使用MotionLayout获得近似值非常简单。我们首先从布局文件开始:
<?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"
tools:context=".MainActivity"
app:layoutDescription="@xml/collapsing_toolbar"
tools:showPaths="true">
<androidx.recyclerview.widget.RecyclerView
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_image" />
<ImageView
android:id="@+id/toolbar_image"
android:layout_width="0dp"
android:layout_height="200dp"
android:adjustViewBounds="true"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:fitsSystemWindows="true"
android:scaleType="center"
android:src="@drawable/beach_huts"
android:background="@color/colorPrimary" />
<ImageView
android:id="@android:id/home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:src="@drawable/abc_ic_ab_back_material"
android:tint="?android:attr/textColorPrimaryInverse"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="24dp"
android:text="@string/app_name"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>
这本质上是我们可以使用ConstraintLayout创建的标准布局,唯一的区别是父实际上是MotionLayout(它扩展了ConstraintLayout,所以我们可以像普通的ConstraintLayout一样使用MotionLayout)。该MotionLayout有一个名为属性这哪里是奇迹发生。我故意在这里使用基本的View类型来清楚地表明没有来自Views本身的行为。在真正的应用程序中,我将使用AppBarLayout和工具栏。app:layoutDescription
如果我们在设计工具中查看它,我们可以看到这表示工具栏处于展开状态时的布局:
activity_main.png我刚才提到魔法发生在app:layoutDescription属性中引用的文件中,所以让我们来看看:
<?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/expanded">
<Constraint
android:id="@id/toolbar_image"
android:layout_height="200dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="imageAlpha"
app:customIntegerValue="255" />
</Constraint>
<Constraint
android:id="@id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="24dp"
android:scaleX="1.0"
android:scaleY="1.0"
app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
app:layout_constraintStart_toStartOf="parent">
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/collapsed">
<Constraint
android:id="@id/toolbar_image"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="imageAlpha"
app:customIntegerValue="0" />
</Constraint>
<Constraint
android:id="@id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginBottom="0dp"
android:scaleX="0.625"
android:scaleY="0.625"
app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/toolbar_image">
</Constraint>
</ConstraintSet>
</MotionScene>
所以这对MotionLayout来说是全新的,可能看起来有些可怕,所以让我们把它分解成更小,更易于管理的块。父布局是一个MotionScene,它包含定义转换的所有组件。它包含两个ConstraintSet,每个ConstraintSet定义一组约束,表示布局的固定状态。我们稍后将详细介绍这些内容,但是现在只需要了解一个ConstraintSet表示工具栏处于完全展开状态,另一个表示工具栏处于完全折叠状态。
所述过渡元素定义什么这些开始和结束状态是,如何在两者之间的转换由用户交互来控制:
<Transition
app:constraintSetEnd="@id/collapsed"
app:constraintSetStart="@id/expanded">
<OnSwipe
app:dragDirection="dragUp"
app:touchAnchorId="@id/recyclerview"
app:touchAnchorSide="top" />
</Transition>
在app:constraintSetStart和app:constraintSetEnd属性是两个引用ConstrainSet定义的展开和折叠状态s。该OnSwipe元素结合在转变到用户的拖动RecyclerView在我们前面的主要布局文件。在展开和折叠状态下,RecyclerView的顶部边缘位于不同的位置,因为它被限制在带有ID 的ImageView的底部边缘toolbar_image,并且这种转换完全是关于控制该变量位置,并且该控制来自用户在RecyclerView上拖动。在这10行XML中,我们正在完成大量的工作。内部结构非常复杂,因为它被驱逐出RecyclerView的滚动行为。
要理解两个ConstrainSet定义,让我们首先考虑我们需要控制的两件事。第一个是ImageView,它表示背景(带ID toolbar_image)更改高度,图像不透明度发生变化。通过更改高度,它还将导致RecyclerView的顶部移动,因为后者被约束到此ImageView的底部。第二个视图是TextView,它包含title需要移动和更改大小的标题(带ID )。
让我们首先看一下ImageView的两个状态之间的差异。在扩展状态下,它是这样的:
<ConstraintSet android:id="@+id/expanded">
<Constraint
android:id="@id/toolbar_image"
android:layout_height="200dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="imageAlpha"
app:customIntegerValue="255" />
</Constraint>
因此
<ConstraintSet android:id="@+id/collapsed">
<Constraint
android:id="@id/toolbar_image"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<CustomAttribute
app:attributeName="imageAlpha"
app:customIntegerValue="0" />
</Constraint>
这里只有两个小的差异。首先是layout_height
,第二个是CustomAttribute命名imageAlpha
。CustomAttribute这个名称可能意味着我们正在使用自定义View,但事实并非如此。虽然我们使用一个标准的ImageView,在主属性约束的元素约束集可以是任何的属性ConstraintLayout.LayoutParams或任何属性的查看,但查看的子类,如ImageView的,我们需要使用一个CustomAttribute实际上与ObjectAnimator非常相似。在这种情况下,我们正在调整imageAlphaImageView的属性。当然,您也可以将此技术用于自定义视图的自定义属性,就像ObjectAnimator一样。
TextView实际上非常相似。扩展状态是:
<Constraint
android:id="@id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="24dp"
android:scaleX="1.0"
android:scaleY="1.0"
app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
app:layout_constraintStart_toStartOf="parent" />
崩溃的状态是:
<Constraint
android:id="@id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginBottom="0dp"
android:scaleX="0.625"
android:scaleY="0.625"
app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/toolbar_image"/>
这里我们使用视图缩放来改变TextView的大小。如果您想知道为什么我选择了视图缩放而不是textSize通过CustomAttribute进行更改,原因是更改文本大小并重新渲染它在计算上比仅仅应用转换要昂贵得多,因此我们不太可能使用这种技术获得过渡。
我们正在做的另一件事是改变边距,以及TextView相对于ImageView的定位方式。在折叠状态下,它垂直居中,在展开状态下,它与底部对齐,因此TextView将更加相对于ImageView的大小。
如果我们使用该布局代替我们开始的CoordinatorLayout实现,我们会得到以下行为:
motion_basic.gif这实际上非常接近,但是鹰眼可能会发现它与我们在开始时看到的CoordinatorLayout方法之间存在细微差别:在CoordinatorLayout转换中,图像淡入不会在转换过程中发生,因为它与MotionLayout版本。在本系列的结束文章中,我们将介绍一些使用MotionLayout可以获得的更细粒度的控件。
这里提供了本文的源代码。
网友评论