MontionLayout:打開動畫新世界大門(其一)

最初接觸到 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

固然,你也能夠按照字面意思將它簡單理解爲「運動佈局」。爲何這麼說呢?經過上圖來對比傳統的佈局組件(如:FrameLayoutLinearLayout 等),咱們不難發現:MotionLayout 是佈局組件中的一個「里程碑」,由此開始就告別了 XML 文件中只能」靜態「操做 UI 的歷史。經過 MotionLayout,咱們就能更加輕易處理其內部子 View 的手勢操做和"運動"效果了。正如 Nicolas Roard 所說的那樣:app

你能夠在 MontionLayout 功能方面將其看做是屬性動畫、TransitionManager 和 CoordinatorLayout 的結合體編輯器

MotionLayout 基礎

首先,咱們須要從 MotionLayout 的一些基本屬性和用法講起,這樣對於咱們後面的實際操做將會頗有幫助。ide

引入 MotionLayout 庫

dependencies {
    implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta2'
}
複製代碼

目前,MotionLayout 仍處於 beta 版本,雖然官方以前說過 MotionLayout 的動畫輔助工具將會在 beta 版本推出,但目前尚未出現,不出意外應該是在下一個版本了。到時候應該就能夠像 ConstraintLayout 那樣直接經過佈局編輯器來進行部分預覽和參數操做了。工具

在佈局文件中使用 MotionLayout

想要使用 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?結合上圖 MotionScene 主要由三部分組成:StateSetConstraintSetTransition。爲了讓你們快速理解和使用 MotionScene,本文將重點講解 ConstarintSetTransition,至於 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 設置它的 MotionScenestep1,接下來就讓咱們一睹 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 爲藍色小機器人便可,怎麼樣,是否是有種「拍案驚奇」的感受😁。此外,你能夠經過指定 touchAnchorSidedragDirection 等來指定本身想要的滑動手勢和滑動方向,默認爲向上滑動,手勢滑動咱們將在後面示例中穿插使用和講解,這裏不作具體介紹,忍不住的小夥伴能夠去查看一下官方文檔介紹。OK,就這樣,咱們上面的僞「百花齊放」效果就已經實現了,沒什麼難的對吧😄。

到這裏,你可能會說:前面兩個示例的動畫軌跡一直是"直線",若是想要某段動畫過程的軌跡是"曲線"效果能夠嗎?固然沒問題!Keyframes 關鍵幀幫你安排!

KeyFrameSet:讓動畫獨樹一幟

若是咱們想實現「獨樹一幟」的動畫交互效果,那就離不開 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 即爲動畫中點位置。另外,能夠經過指定 percentXpercentY 來設置該關鍵幀位置的偏移量,它們取值通常爲 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)

Defining motion paths in MotionLayout

MotionLayout development

最後

本文的出發點是但願僅僅爲你們提供一個「鑰匙孔」的角色,經過這個「孔」,你們能夠依稀看見門裏「寶藏」的餘光,想要打開門尋得寶藏,就須要你們"事必躬親",拿到「鑰匙」來打開這扇門了😄。固然,你們也能夠繼續關注個人後續之做,來發現更多 MontionLayout 的寶藏。

相關文章
相關標籤/搜索