最初接觸到 MotionLayout 是在國外知名博客的 Android 專欄上。第一眼見到 MotionLayout
時無疑是興奮的,在通過使用和熟悉了這個佈局組件以後,我就想將這份喜悅傳遞給國內開發者,今後「拳打」設計,「腳踢」產品😁。固然,因爲關於 MotionLayout
的外文專欄相關介紹已足夠詳細,因此本文僅對其進行總結和簡單應用。老規矩,正文開始前先上一張圖:php
因爲本文的受衆須要有一點 ConstraintLayout
的用法基礎,若是你對它並不熟悉,能夠先去花幾分鐘看一下本人以前的譯文:帶你領略 ConstraintLayout 1.1 的新功能。回到正題,什麼是 MontionLayout ?不少人可能會對這個名詞比較陌生,但若是說到它的前身 — ConstraintLayout
,你們應該就多少有些瞭解了。MontionLayout 實際上是 Google 在去年開發者大會上新推的佈局組件。咱們先來看看 Android 官方對於它的定義:android
MotionLayout is a layout type that helps you manage motion and widget animation in your app. MotionLayout is a subclass of
ConstraintLayout
and builds upon its rich layout capabilities.git
簡單翻譯過來就是:MontionLayout
是一個可以幫助咱們在 app 中管理手勢和控件動畫的佈局組件。它是 ConstraintLayout
的子類而且基於它自身豐富的佈局功能來進行構建。github
固然,你也能夠按照字面意思將它簡單理解爲「運動佈局」。爲何這麼說呢?經過上圖來對比傳統的佈局組件(如:FrameLayout
、LinearLayout
等),咱們不難發現:MotionLayout
是佈局組件中的一個「里程碑」,由此開始就告別了 XML 文件中只能」靜態「操做 UI 的歷史。經過 MotionLayout
,咱們就能更加輕易處理其內部子 View
的手勢操做和"運動"效果了。正如 Nicolas Roard 所說的那樣:app
你能夠在 MontionLayout 功能方面將其看做是屬性動畫、TransitionManager 和 CoordinatorLayout 的結合體。編輯器
首先,咱們須要從 MotionLayout
的一些基本屬性和用法講起,這樣對於咱們後面的實際操做將會頗有幫助。ide
dependencies {
implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta2'
}
複製代碼
目前,MotionLayout
仍處於 beta
版本,雖然官方以前說過 MotionLayout
的動畫輔助工具將會在 beta
版本推出,但目前尚未出現,不出意外應該是在下一個版本了。到時候應該就能夠像 ConstraintLayout
那樣直接經過佈局編輯器來進行部分預覽和參數操做了。工具
想要使用 MotionLayout
,只須要在佈局文件中做以下聲明便可:佈局
<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/scene1">
</android.support.constraint.motion.MotionLayout>
複製代碼
因爲 MotionLayout
做爲 ConstraintLayout
的子類,那麼就天然而然地能夠像 ConstraintLayout
那樣使用去「約束」子視圖了,不過這可就有點「大材小用了」,MotionLayout
的用處可遠不止這些。咱們先來看看 MotionLayout 的構成:post
由上圖可知,MotionLayout 可分爲 <View>
和 <Helper>
兩個部分。<View>
部分可簡單理解爲一個 ConstraintLayout
,至於 <Helper>
其實就是咱們的「動畫層」了。MotionLayout 爲咱們提供了 layoutDescription
屬性,咱們須要爲它傳入一個 MotionScene
包裹的 XML
文件,想要實現動畫交互,就必須經過這個「媒介」來鏈接。
什麼是 MotionScene?結合上圖 MotionScene 主要由三部分組成:StateSet
、ConstraintSet
和 Transition
。爲了讓你們快速理解和使用 MotionScene,本文將重點講解 ConstarintSet
和 Transition
,至於 StateSet
狀態管理將會在後續文章中爲你們介紹具體用法和場景。同時,爲了幫助你們理解,此處將開始結合一些具體小實例來幫助你們快速理解和使用它。
首先,咱們從實現下面這個簡單的效果講起:
GIF 畫質有點渣,見諒,但從上圖咱們能夠發現這是一個簡單的平移動畫,經過點擊自身(籃球)來觸發,讓咱們來經過 MotionLayout 的方式來實現它。首先來看下佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutDescription="@xml/step1" tools:context=".practice.MotionSampleActivity">
<ImageView android:id="@+id/ball" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_basketball"/>
</android.support.constraint.motion.MotionLayout>
複製代碼
佈局文件很簡單,只不過你可能會注意到,咱們對 ImageView
並無添加任何約束,緣由在於:咱們會在 MotionScene
中聲明 ConstraintSet
,裏面將包含該 ImageView
的「運動」起始點和終點的約束信息。固然你也能夠在佈局文件中對其加以約束,但 MotionScene
中對於控件約束的優先級會高於佈局文件中的設定。這裏咱們經過 layoutDescription
來爲 MotionLayout
設置它的 MotionScene
爲 step1
,接下來就讓咱們一睹 MotionScene
的芳容:
<?xml version="1.0" encoding="utf-8"?>
<!--describe the animation for activity_motion_sample_step1.xml-->
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- A transition describes an animation via start and end state -->
<Transition app:constraintSetStart="@id/start" app:constraintSetEnd="@id/end" app:duration="2200">
<OnClick app:targetId="@id/ball" app:clickAction="toggle" />
</Transition>
<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@+id/ball" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginStart="12dp" android:layout_marginTop="12dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">
<Constraint android:id="@+id/ball" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginEnd="12dp" android:layout_marginBottom="12dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
</ConstraintSet>
</MotionScene>
複製代碼
首先,能夠發現咱們定義了兩個 <ConstraintSet>
,分別描述了這個🏀 ImageView
的動畫起始位置以及結束位置的約束信息(僅包含少許必要信息,如:width、height、margin以及位置屬性等)。顯而易見,籃球的起始位置爲屏幕左上角,結束位置爲屏幕右下角,那麼問題來了,如何讓它動起來呢?這就要依靠咱們的 <Transition>
元素了。事實上,咱們都知道,動畫都是有開始位置和結束位置的,而 MotionLayout
正是利用這一客觀事實,將首尾位置和動畫過程分離,兩個點位置和距離雖然是固定的,可是它們之間的 Path 是無限的,能夠是「一馬平川」,也能夠是"蜿蜒曲折"的。
回到上面這個例子,咱們只須要爲 Transition
設置起始位置和結束位置的 ConstraintSet
並設置動畫時間便可,剩下的都交給 MotionLayout
自動去幫咱們完成。固然你也能夠經過 onClick
點擊事件來觸發動畫,綁定目標控件的 id 以及經過 clickAction
屬性來設置點擊事件的類型,這裏咱們設置的是 toggle
,即經過反覆點擊控件來切換動畫的狀態,其餘還有不少屬性能夠參照官方文檔去研究,比較簡單,這裏就不一一講解它們的效果了。如此一來,運行一下就能看到上面的效果了。另外,爲了方便測試,咱們能夠給 MotionLayout
加上調試屬性:app:motionDebug="SHOW_PATH"
,而後就能輕易的查看其動畫內部的運動軌跡:
什麼?你說這個動畫效果太基礎?那好,我就來個簡陋版的「百花齊放」效果吧,好比下面這樣:
首先,讓咱們分析一下這個效果:仔細看咱們能夠發現,經過向上滑動藍色的 Android 機器人,紫色和橙色的機器人會慢慢淡出並分別忘左上角和右上角移動。佈局文件很簡單,一把梭就OK了😂:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.motion.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" app:motionDebug="SHOW_PATH" app:layoutDescription="@xml/step2" tools:context=".practice.MotionSampleActivity">
<ImageView android:id="@+id/ic_android_blue" android:layout_width="42dp" android:layout_height="42dp" android:src="@mipmap/android_icon_blue"/>
<ImageView android:id="@+id/ic_android_left" android:layout_width="42dp" android:layout_height="42dp" android:src="@mipmap/android_icon_purple"/>
<ImageView android:id="@+id/ic_android_right" android:layout_width="42dp" android:layout_height="42dp" android:src="@mipmap/android_icon_orange"/>
<TextView android:id="@+id/tipText" android:text="Swipe the blue android icon up" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="16dp" android:layout_marginTop="16dp" app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.motion.MotionLayout>
複製代碼
下面咱們來看下 step2
中的 MotionScene:
<?xml version="1.0" encoding="utf-8"?>
<!--describe the animation for activity_motion_sample_step2.xml-->
<!--animate by dragging target view-->
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<!--At the start, all three stars are centered at the bottom of the screen.-->
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@+id/ic_android_blue" android:layout_width="42dp" android:layout_height="42dp" android:layout_marginBottom="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
<Constraint android:id="@+id/ic_android_left" android:layout_width="42dp" android:layout_height="42dp" android:alpha="0.0" android:layout_marginBottom="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
<Constraint android:id="@+id/ic_android_right" android:layout_width="42dp" android:layout_height="42dp" android:layout_marginBottom="20dp" android:alpha="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
</ConstraintSet>
<!--Define the end constraint to set use a chain to position all three stars together below @id/tipText.-->
<ConstraintSet android:id="@+id/end">
<Constraint android:id="@+id/ic_android_left" android:layout_width="58dp" android:layout_height="58dp" android:layout_marginEnd="90dp" android:alpha="1.0" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/ic_android_blue" app:layout_constraintTop_toBottomOf="@id/tipText"/>
<Constraint android:id="@+id/ic_android_blue" android:layout_width="58dp" android:layout_height="58dp" app:layout_constraintEnd_toStartOf="@id/ic_android_right" app:layout_constraintStart_toEndOf="@id/ic_android_left" app:layout_constraintTop_toBottomOf="@id/tipText"/>
<Constraint android:id="@+id/ic_android_right" android:layout_width="58dp" android:layout_height="58dp" android:layout_marginStart="90dp" android:alpha="1.0" app:layout_constraintStart_toEndOf="@id/ic_android_blue" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/tipText"/>
</ConstraintSet>
<!-- A transition describes an animation via start and end state -->
<Transition app:constraintSetStart="@id/start" app:constraintSetEnd="@id/end">
<!-- MotionLayout will track swipes relative to this view -->
<OnSwipe app:touchAnchorId="@id/ic_android_blue"/>
</Transition>
</MotionScene>
複製代碼
上面代碼其實很好理解,以前咱們定義了一個控件的 Constraint
,如今只須要多加兩個便可。因爲三個 Android 機器人起點位置是同樣的,而只有藍色的顯示,那麼只要在開始位置將另外的兩個機器人透明度設置爲 0 便可,而後在結束位置將三個小機器人分開擺放,這裏設計到 ConstraintLayout
的基礎,就很少說了。接着將結束位置的左、右 Android 機器人透明度設置爲 1,動畫開始後,MotionLayout
會自動處理目標控件 alpha 屬性的變化效果,讓其看起來依舊絲滑。
另外,咱們這裏沒有再經過 <OnClick>
來觸發動畫效果,相似的,咱們使用了 <OnSwipe>
手勢滑動來觸發動畫,只須要指定 touchAnchorId
爲藍色小機器人便可,怎麼樣,是否是有種「拍案驚奇」的感受😁。此外,你能夠經過指定 touchAnchorSide
和 dragDirection
等來指定本身想要的滑動手勢和滑動方向,默認爲向上滑動,手勢滑動咱們將在後面示例中穿插使用和講解,這裏不作具體介紹,忍不住的小夥伴能夠去查看一下官方文檔介紹。OK,就這樣,咱們上面的僞「百花齊放」效果就已經實現了,沒什麼難的對吧😄。
到這裏,你可能會說:前面兩個示例的動畫軌跡一直是"直線",若是想要某段動畫過程的軌跡是"曲線"效果能夠嗎?固然沒問題!Keyframes 關鍵幀幫你安排!
若是咱們想實現「獨樹一幟」的動畫交互效果,那就離不開 KeyFrameSet 這個強大的屬性。它能夠改變咱們動畫過程當中某個關鍵幀的位置以及狀態信息。這樣說可能不太好理解,咱們先來看下面這個示例:
以你們的慧眼不難發現:風車的運動軌跡爲曲線,而且旋轉並放大至中間位置時會達到零界點,而後開始縮小。佈局代碼就不上了,很簡單,裏面惟一重要的就是咱們須要實現的 MontionScene 效果 — step3.xml
了:
<?xml version="1.0" encoding="utf-8"?>
<!--describe the animation for activity_motion_sample_step3.xml-->
<!--animate in the path way on a view-->
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- Constraints to apply at the start of the animation -->
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@id/windmill" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginStart="12dp" android:layout_marginBottom="12dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
<Constraint android:id="@id/tipText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:alpha="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@id/windmill" app:layout_constraintTop_toTopOf="@id/windmill"/>
</ConstraintSet>
<!-- Constraints to apply at the end of the animation -->
<ConstraintSet android:id="@+id/end">
<!--this view end point should be at bottom of parent-->
<Constraint android:id="@id/windmill" android:layout_width="40dp" android:layout_height="40dp" android:layout_marginBottom="12dp" android:layout_marginEnd="12dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
<Constraint android:id="@+id/tipText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:alpha="1.0" android:layout_marginEnd="72dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/>
</ConstraintSet>
<!-- A transition describes an animation via start and end state -->
<Transition app:constraintSetStart="@id/start" app:constraintSetEnd="@id/end">
<KeyFrameSet>
<KeyPosition app:framePosition="50" app:motionTarget="@id/windmill" app:keyPositionType="parentRelative" app:percentY="0.5"/>
<!--apply other animation attributes-->
<!--前半段的動畫效果:逆時針旋轉一圈,同時放大一倍-->
<KeyAttribute app:motionTarget="@id/windmill" android:rotation="-360" android:scaleX="2.0" android:scaleY="2.0" app:framePosition="50"/>
<!--後半段的動畫效果:逆時針旋轉一圈,同時變回原樣-->
<KeyAttribute app:motionTarget="@id/windmill" android:rotation="-720" app:framePosition="100"/>
<!--延遲動畫——0-85過程當中將透明度一直維持在0.0-->
<KeyAttribute app:motionTarget="@id/tipText" app:framePosition="85" android:alpha="0.0"/>
</KeyFrameSet>
<OnSwipe app:touchAnchorId="@id/windmill" app:touchAnchorSide="bottom" app:dragDirection="dragRight"/>
</Transition>
</MotionScene>
複製代碼
從上述代碼咱們能夠發現:KeyFrameSet
須要被包含在 Transition
裏面,同時 KeyFrameSet
中定義了 <KeyPosition>
和 <KeyAttribute>
兩種元素,它們主要用來設置動畫某個位置的關鍵幀,進而爲某段動畫指定所指望的效果。顧名思義,KeyPosition
用於指定動畫某個關鍵幀的位置信息,而 KeyAttribute
則用來描述動畫某關鍵幀的屬性配置(如:透明度、縮放、旋轉等)。除此之外,KeyFrameSet
中還支持 <KeyCycle>
和 <KeyTimeCycle>
來讓動畫變得更加有趣和靈活,因篇幅有限,將在後續文章對兩者進行講解。
咱們先來看下 KeyPosition
的構成:
從上圖可見,keyPositionType
一共有三種,本文使用的是 parentRelative
,即以整個 MotionLayout
的佈局爲座標系,左上角爲座標原點,即參考 View 的座標系便可,而另外兩種將在後續文章統一講解和應用,它們的區別在於座標系選取的參考點不一樣而已。咱們經過 framePosition
屬性來指定關鍵幀所在的位置,取值範圍爲 0 - 100
,本示例中設置的 50
即爲動畫中點位置。另外,能夠經過指定 percentX
和 percentY
來設置該關鍵幀位置的偏移量,它們取值通常爲 0 — 1
,固然也能夠設置爲負數或者大於一,好比,本示例中若是沒有設置偏移量,那麼動畫的軌跡無疑是一條平行於 x 軸的直線,但經過設置 app:percentY="0.5"
,那麼風車就會在動畫中點位置向 y 軸方向偏移一半的高度,即下圖的效果(開始 debug
模式):
可能會有人問了:爲何軌跡不是三角形,而是曲線呢?哈哈,這個問題問得好!由於 MotionLayout
會自動地將關鍵幀位置儘可能銜接的圓滑,讓動畫執行起來不那麼僵硬。其餘代碼應該就比較好理解了,能夠參照文檔理解。
瞭解完 KeyFrameSet
的用法,那麼咱們就很輕易的實現下面這個效果啦:
代碼就不貼了,MotionLayout
系列代碼都會上傳至 GitHub 上,感興趣的小夥伴能夠去看一下。不知不覺已經講了這麼多,但發現還有不少內容沒有涉及到或是講清楚,因爲篇幅有限,就只能放在後面幾期來爲你們介紹啦😄。若是你們以爲對本文有什麼問題或者建議,歡迎評論區留言,知無不言,言無不盡。
本文所有代碼:github.com/Moosphan/Co…
後續文章將繼續跟進相關進階用法,該倉庫也將持續更新,敬請期待~
Introduction to MotionLayout (part I)
Introduction to MotionLayout (part II)
Introduction to MotionLayout (part III)
本文的出發點是但願僅僅爲你們提供一個「鑰匙孔」的角色,經過這個「孔」,你們能夠依稀看見門裏「寶藏」的餘光,想要打開門尋得寶藏,就須要你們"事必躬親",拿到「鑰匙」來打開這扇門了😄。固然,你們也能夠繼續關注個人後續之做,來發現更多
MontionLayout
的寶藏。