在Google IO 2018上宣布了ConstraintLayout 2.0碰酝,最大的新增功能是MotionLayout,它為我們提供了一個(gè)用于布局動(dòng)畫(huà)的驚人新工具使兔。Nicolas Roard已經(jīng)發(fā)表了對(duì)MotionLayout的精彩介紹默色,我強(qiáng)烈建議您閱讀一下,以了解MotionLayout的基本知識(shí)和組件糕篇。在這個(gè)簡(jiǎn)短的系列中啄育,我們將看看如何使用MotionLayout創(chuàng)建一個(gè)我們都應(yīng)該熟悉的行為:折疊工具欄。
在開(kāi)始之前拌消,值得一提的是在CoordinatorLayout中使用CollapsingToolbarLayout來(lái)實(shí)現(xiàn)此行為并沒(méi)有任何錯(cuò)誤挑豌。此外,如果您已經(jīng)在應(yīng)用程序中工作墩崩,那么通過(guò)更改幾乎無(wú)法獲得氓英。也就是說(shuō),哪個(gè)CoordinatorLayout提供了一些非常有用的行為鹦筹,試圖調(diào)整它們甚至創(chuàng)建自己的自定義行為是相當(dāng)困難的铝阐。正是在這種情況下,MotionLayout可以提供更大的靈活性铐拐,而且我的早期實(shí)驗(yàn)表明徘键,實(shí)現(xiàn)您想要的更容易。此外遍蟋,MotionLayout開(kāi)辟了一些非常難以實(shí)現(xiàn)的新行為CoordinatorLayout吹害。
MotionLayout與Android上許多其他動(dòng)畫(huà)框架之間的主要區(qū)別之一是視圖動(dòng)畫(huà)和屬性動(dòng)畫(huà)在給定的持續(xù)時(shí)間內(nèi)運(yùn)行。雖然可以指定持續(xù)時(shí)間并取消正在運(yùn)行的動(dòng)畫(huà)虚青,但是無(wú)法根據(jù)用戶(hù)輸入控制正在運(yùn)行的動(dòng)畫(huà)它呀。例如,折疊工具欄應(yīng)根據(jù)用戶(hù)滾動(dòng)進(jìn)行展開(kāi)和折疊,實(shí)際動(dòng)畫(huà)應(yīng)遵循用戶(hù)的拖動(dòng)纵穿。這些框架根本不可能實(shí)現(xiàn)這一點(diǎn)下隧。
讓我們首先看看我們?cè)噲D模仿的行為。這是一個(gè)折疊工具欄谓媒,它使用材料組件庫(kù)和CoordinatorLayout中的CollapsingToolbarLayout實(shí)現(xiàn):
<?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>
我們從中獲得的行為是這樣的:
使用MotionLayout獲得近似值非常簡(jiǎn)單淆院。我們首先從布局文件開(kāi)始:
<?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>
這本質(zhì)上是我們可以使用ConstraintLayout創(chuàng)建的標(biāo)準(zhǔn)布局,唯一的區(qū)別是父實(shí)際上是MotionLayout(它擴(kuò)展了ConstraintLayout篙耗,所以我們可以像普通的ConstraintLayout一樣使用MotionLayout)迫筑。該MotionLayout有一個(gè)名為屬性這哪里是奇跡發(fā)生。我故意在這里使用基本的View類(lèi)型來(lái)清楚地表明沒(méi)有來(lái)自Views本身的行為宗弯。在真正的應(yīng)用程序中脯燃,我將使用AppBarLayout和工具欄。app:layoutDescription
如果我們?cè)谠O(shè)計(jì)工具中查看它蒙保,我們可以看到這表示工具欄處于展開(kāi)狀態(tài)時(shí)的布局:
我剛才提到魔法發(fā)生在app:layoutDescription屬性中引用的文件中辕棚,所以讓我們來(lái)看看:
<?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>
所以這對(duì)MotionLayout來(lái)說(shuō)是全新的,可能看起來(lái)有些可怕邓厕,所以讓我們把它分解成更小逝嚎,更易于管理的塊。父布局是一個(gè)MotionScene详恼,它包含定義轉(zhuǎn)換的所有組件补君。它包含兩個(gè)ConstraintSet,每個(gè)ConstraintSet定義一組約束昧互,表示布局的固定狀態(tài)挽铁。我們稍后將詳細(xì)介紹這些內(nèi)容,但是現(xiàn)在只需要了解一個(gè)ConstraintSet表示工具欄處于完全展開(kāi)狀態(tài)敞掘,另一個(gè)表示工具欄處于完全折疊狀態(tài)叽掘。
所述過(guò)渡元素定義什么這些開(kāi)始和結(jié)束狀態(tài)是,如何在兩者之間的轉(zhuǎn)換由用戶(hù)交互來(lái)控制:
<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屬性是兩個(gè)引用ConstrainSet定義的展開(kāi)和折疊狀態(tài)s玖雁。該OnSwipe元素結(jié)合在轉(zhuǎn)變到用戶(hù)的拖動(dòng)RecyclerView在我們前面的主要布局文件更扁。在展開(kāi)和折疊狀態(tài)下,RecyclerView的頂部邊緣位于不同的位置赫冬,因?yàn)樗幌拗圃趲в蠭D 的ImageView的底部邊緣toolbar_image浓镜,并且這種轉(zhuǎn)換完全是關(guān)于控制該變量位置,并且該控制來(lái)自用戶(hù)在RecyclerView上拖動(dòng)劲厌。在這10行XML中竖哩,我們正在完成大量的工作。內(nèi)部結(jié)構(gòu)非常復(fù)雜脊僚,因?yàn)樗或?qū)逐出RecyclerView的滾動(dòng)行為。
要理解兩個(gè)ConstrainSet定義,讓我們首先考慮我們需要控制的兩件事辽幌。第一個(gè)是ImageView增淹,它表示背景(帶ID toolbar_image)更改高度,圖像不透明度發(fā)生變化乌企。通過(guò)更改高度虑润,它還將導(dǎo)致RecyclerView的頂部移動(dòng),因?yàn)楹笳弑患s束到此ImageView的底部加酵。第二個(gè)視圖是TextView拳喻,它包含title需要移動(dòng)和更改大小的標(biāo)題(帶ID )。
讓我們首先看一下ImageView的兩個(gè)狀態(tài)之間的差異猪腕。在擴(kuò)展?fàn)顟B(tài)下冗澈,它是這樣的:
<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>
這里只有兩個(gè)小的差異。首先是layout_height
陋葡,第二個(gè)是CustomAttribute命名imageAlpha
亚亲。CustomAttribute這個(gè)名稱(chēng)可能意味著我們正在使用自定義View,但事實(shí)并非如此腐缤。雖然我們使用一個(gè)標(biāo)準(zhǔn)的ImageView捌归,在主屬性約束的元素約束集可以是任何的屬性ConstraintLayout.LayoutParams或任何屬性的查看,但查看的子類(lèi)岭粤,如ImageView的惜索,我們需要使用一個(gè)CustomAttribute實(shí)際上與ObjectAnimator非常相似。在這種情況下剃浇,我們正在調(diào)整imageAlphaImageView的屬性巾兆。當(dāng)然,您也可以將此技術(shù)用于自定義視圖的自定義屬性偿渡,就像ObjectAnimator一樣臼寄。
TextView實(shí)際上非常相似。擴(kuò)展?fàn)顟B(tài)是:
<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" />
崩潰的狀態(tài)是:
<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"/>
這里我們使用視圖縮放來(lái)改變TextView的大小溜宽。如果您想知道為什么我選擇了視圖縮放而不是textSize通過(guò)CustomAttribute進(jìn)行更改吉拳,原因是更改文本大小并重新渲染它在計(jì)算上比僅僅應(yīng)用轉(zhuǎn)換要昂貴得多,因此我們不太可能使用這種技術(shù)獲得過(guò)渡适揉。
我們正在做的另一件事是改變邊距留攒,以及TextView相對(duì)于ImageView的定位方式。在折疊狀態(tài)下嫉嘀,它垂直居中炼邀,在展開(kāi)狀態(tài)下,它與底部對(duì)齊剪侮,因此TextView將更加相對(duì)于ImageView的大小拭宁。
如果我們使用該布局代替我們開(kāi)始的CoordinatorLayout實(shí)現(xiàn)洛退,我們會(huì)得到以下行為:
這實(shí)際上非常接近,但是鷹眼可能會(huì)發(fā)現(xiàn)它與我們?cè)陂_(kāi)始時(shí)看到的CoordinatorLayout方法之間存在細(xì)微差別:在CoordinatorLayout轉(zhuǎn)換中杰标,圖像淡入不會(huì)在轉(zhuǎn)換過(guò)程中發(fā)生兵怯,因?yàn)樗cMotionLayout版本。在本系列的結(jié)束文章中腔剂,我們將介紹一些使用MotionLayout可以獲得的更細(xì)粒度的控件媒区。
這里提供了本文的源代碼。