MotionLayout使用指南

在傳統Android開發中,創建流暢的交互式動畫往往需要組合使用屬性動畫、TransitionManager或CoordinatorLayout等多種技術,代碼量大且維護困難。Google在ConstraintLayout 2.0中推出的MotionLayout成功解決了這一痛點,它將佈局容器與動畫描述分離,通過聲明式的XML配置即可實現複雜的交互動畫MotionLayout兼具了屬性動畫的靈活性、TransitionManager的佈局過渡能力以及CoordinatorLayout的觸摸響應特性,成為一個功能強大的全能型動畫解決方案。

一、MotionLayout與MotionScene

MotionLayout本身是一個ViewGroup(視圖容器),而MotionScene是其核心的"大腦",專門用於定義和控制動畫

MotionLayout實際是ConstraintLayout的子類,它首先是一個佈局容器,負責承載界面元素。同時,它也是一個動畫引擎,實時解析和執行MotionScene中的指令。

MotionScene:是一個獨立的XML資源文件(通常位於res/xml目錄),作為動畫的"劇本",詳細描述動畫的各種狀態和過渡過程。MotionLayout通過app:layoutDescription屬性與MotionScene文件關聯。

這種分離設計讓動畫邏輯與佈局容器解耦,大大提高了代碼的可維護性和可讀性。

二、項目配置與創建方式

2.1 添加依賴

要使用MotionLayout,首先需要在項目的build.gradle文件中添加ConstraintLayout 2.0及以上版本的依賴

dependencies {
    implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
}

2.2 創建方式

  • 最直接的方式是對佈局進行自動轉換。在Android Studio中,打開已有的ConstraintLayout佈局文件,在Component Tree中右鍵點擊根佈局,選擇"Convert to MotionLayout"。
    這種方式Android Studio會自動將ConstraintLayout替換為MotionLayout,並在res/xml目錄下創建對應的MotionScene文件,同時通過app:layoutDescription屬性建立兩者的關聯關係。

MotionLayout 基礎篇_xml

  • 也可以自行手動創建,手動修改佈局文件的根標籤為MotionLayout或者創建文件時指定MotionLayout的根佈局,但是這種做法需要我們自己在res/xml文件下創建對應的MotionScene文件並進行關聯。

三、MotionScene的三大核心組件

MotionScene由三個基本組成部分ConstraintSet(約束集)Transition(過渡)和觸發機制構成,理解它們是掌握MotionLayout的關鍵。

3.1 ConstraintSet(約束集)

ConstraintSet定義了動畫的關鍵狀態,通常是開始狀態和結束狀態。每個ConstraintSet詳細描述了在該狀態下,每個視圖的位置、大小、透明度等所有屬性。

在開始的ConstrainSet中,我們可以不對View進行約束,會自動繼承佈局文件中定義的初始約束。但是在<ConstraintSet android:id="@+id/end"> </ConstraintSet>中必須完整寫出View的寬高以及約束等屬性

<ConstraintSet android:id="@+id/start">
    
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
    <Constraint
        android:id="@+id/button"
        android:layout_width="64dp"
        android:layout_height="64dp"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

3.2 Transition(過渡)

Transition定義了動畫的過程,指定了從哪個ConstraintSet開始,過渡到哪個ConstraintSet結束

<Transition
    motion:constraintSetStart="@id/start"
    motion:constraintSetEnd="@+id/end"
    motion:duration="1000">
    
    <!-- 觸發條件 -->
</Transition>

關鍵屬性

  • constraintSetStart:起始狀態ConstraintSet的ID
  • constraintSetEnd:結束狀態ConstraintSet的ID
  • duration:動畫持續時間(毫秒)
KeyFrame(關鍵幀)

我們已經知道ConstraintSet描述了Start、End狀態,那麼動畫過渡的路徑可以是直線,也可以是任意曲線,而這就需要通過KeyFrameSet關鍵幀來設置了。keyFrameSet是MotionLayout中用於定義動畫關鍵幀的容器,它包含多個關鍵幀類型,每種類型可以控制不同的屬性或觸發不同的事件,主要看下面位置和屬性兩種關鍵幀

  • KeyPosition(位置關鍵幀)
    KeyPosition用於定義視圖在動畫路徑上的位置和大小變化,允許通過百分比指定位置、大小、角度等屬性。常用屬性
  • framePosition:一個介於 0 到 100之間的整數,這個數值直接對應動畫完成的百分比
  • motionTarget:目標視圖ID
  • percentX/percentY:位置百分比(0.0-1.0)
  • keyPositionType:座標系類型(parentRelative、pathRelative、deltaRelative)
  • KeyAttribute(屬性關鍵幀)
    KeyPosition用於在動畫的特定時刻動態修改UI元素屬性,如透明度、旋轉、縮放、平移等。常用屬性
  • android:alpha:透明度
  • android:rotation:旋轉角度
  • android:scaleX/scaleY:縮放比例
  • android:translationX/Y:平移距離

再瞭解一下關鍵幀的三種座標系類型

  1. parentRelative(父容器相對座標系),以父容器的左上角為原點(0,0),右下角為(1,1),這是最常用的座標系
  2. deltaRelative(相對偏移座標系),以起始點為原點(0,0),結束點為(1,1),基於相對於起始點和結束點的相對偏移量來計算路徑
  3. pathRelative(路徑相對座標系),基於運動路徑的百分比位置,從起點連接到終點的方向是X軸,對應座標分別是(0,0)和(1,0)

下面的實戰會演示關鍵幀,這裏就不多贅述了。

3.3 觸發機制

Transition中的觸發機制讓動畫與用户交互聯繫起來,一般就是點擊觸發或者滑動觸發。

OnClick(點擊觸發)
<OnClick
    motion:targetId="@id/button"
    motion:clickAction="toggle" />

clickAction常用值

  • toggle:在開始結束狀態之間來回切換。無論當前處於何種狀態,點擊後都會向相反狀態過渡
  • transitionToStart/End:過渡到開始或結束狀態。如果當前已是目標狀態,則點擊無效
  • jumpToStart/End:直接跳轉到開始/結束狀態(無動畫)
OnSwipe(滑動觸發)
<OnSwipe
    motion:touchAnchorId="@id/button"
    motion:touchAnchorSide="right"
    motion:dragDirection="dragRight" />

關鍵屬性

  • touchAnchorId:拖拽錨點視圖
  • dragDirection:拖拽方向(dragLeft/Right/Up/Down)
  • touchAnchorSide:拖拽的錨點邊

四、實戰使用

瞭解了上面的基本概念後,我們就可以開始基本的使用了,

<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:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_scene"
    tools:context=".MainActivity">

    <View
        android:id="@+id/view"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@drawable/closer"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/light_yellow"
        android:text="這是一行文本"
        android:textColor="@color/black"
        android:textSize="25sp"
        app:layout_constraintEnd_toEndOf="@id/view"
        app:layout_constraintStart_toStartOf="@id/view"
        app:layout_constraintTop_toBottomOf="@id/view" />


</androidx.constraintlayout.motion.widget.MotionLayout>

這是活動對應的佈局,和約束佈局基本沒有什麼區別,下面是對應的MotionScene

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">
        <OnClick motion:clickAction="transitionToEnd"
            motion:targetId="@id/text"/>
       <KeyFrameSet>

       </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            motion:layout_constraintBottom_toTopOf="@id/view"
            motion:layout_constraintEnd_toEndOf="@id/view"
            motion:layout_constraintStart_toStartOf="@id/view"/>
    </ConstraintSet>
</MotionScene>

這樣一個最簡單的MotionLayout就成型了,我們實際上只在MotionScene進行了動畫的設置,完全不需要在活動或者碎片中進行任何操作。在觸發機制中設置了TextViewonClick,並且用motion:clickAction="transitionToEnd"規定動畫僅觸發第一次點擊然後移動到終點,後續不會再進行動畫。最後的效果如下。

MotionLayout 基礎篇_xml_02

我們還可以結合關鍵幀,豐富這個動畫的效果。在上面KeyFrameSet中加上下面三個關鍵幀

<KeyFrameSet>
    <!-- 位置關鍵幀:在一半進度時稍微往右上偏一點 -->
    <KeyPosition
        motion:motionTarget="@id/text"
        motion:framePosition="50"
        motion:keyPositionType="deltaRelative"
        motion:percentX="0.8"
        motion:percentY="0.0"/>

    <!-- 屬性關鍵幀:在一半進度時放大 1.5 倍  -->
    <KeyAttribute
        motion:motionTarget="@id/text"
        motion:framePosition="50"
        android:scaleX="1.5"
        android:scaleY="1.5"/>

    <!-- 屬性關鍵幀:在結尾時透明度變為 0.2  -->
    <KeyAttribute
        motion:motionTarget="@id/text"
        motion:framePosition="100"
        android:alpha="0.2"/>
</KeyFrameSet>

最後的效果如下

MotionLayout 基礎篇_xml_03

總結

MotionLayout只需要定義開始狀態 (start)結束狀態 (end) 的佈局約束(ConstraintSet),以及它們之間的過渡(Transition。MotionLayout 會自動計算並執行從一個狀態到另一個狀態的平滑過渡,避免了傳統動畫 尤其是屬性動畫手動操作一個個視圖的具體屬性(如 x, y, rotation)的缺點,這對於複雜的 UI 狀態切換來説,代碼量大且難以維護。同時所有的動畫和交互邏輯都定義在一個單獨的 MotionScene XML 文件中,使得代碼更清晰,Activity/Fragment 中的代碼可以專注於業務邏輯,而不是動畫細節。因此在現代 Android 開發中,除了一些極簡單的動畫,更多時候推薦使用MotionLayout