MotionLayout 基礎教程

溜了溜了發表於2019-04-10

閱讀說明:

  • 本文假設讀者已掌握 ConstraintLayout 的使用。
  • 本文是一篇 MotionLayout 基礎教程,如您已掌握如何使用 MotionLayout,那麼本文可能對您幫助不大。
  • 本文是基於 ConstraintLayout 2.0.0-alpha4 版本編寫的,建議與筆者的版本保持一致。
  • 由於 MotionLayout 官方文件不全,有些知識點是根據筆者自己的理解總結的,如有錯誤,歡迎指正。

新增支援庫:

dependencies {
    ...
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}
複製程式碼

MotionLayout 最低支援到 Android 4.3(API 18),還有就是 MotionLayoutConstraintLayout 2.0 新增的,因此必須確保支援庫的版本不低於 2.0

簡介

MotionLayout 類繼承自 ConstraintLayout 類,允許你為各種狀態之間的佈局設定過渡動畫。由於 MotionLayout 繼承了 ConstraintLayout,因此可以直接在 XML 佈局檔案中使用 MotionLayout 替換 ConstraintLayout

MotionLayout 是完全宣告式的,你可以完全在 XML 檔案中描述一個複雜的過渡動畫而 無需任何程式碼(如果您打算使用程式碼建立過渡動畫,那建議您優先使用屬性動畫,而不是 MotionLayout)。

開始使用

由於 MotionLayout 類繼承自 ConstraintLayout 類,因此可以在佈局中使用 MotionLayout 替換掉 ConstraintLayout

MotionLayoutConstraintLayout 不同的是,MotionLayout 需要連結到一個 MotionScene 檔案。使用 MotionLayoutapp:layoutDescription 屬性將 MotionLayout 連線到一個 MotionScene 檔案。

例:

<?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"
    
    app:layoutDescription="@xml/scene_01">
    
    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
複製程式碼

注意!必須為 MotionLayout 佈局的所有直接子 View 都設定一個 Id(允許不為非直接子 View 設定 Id)。

MotionScene 檔案

MotionScene 檔案描述了兩個場景間的過渡動畫,存放在 res/xml 目錄下。

要使用 MotionLayout 建立過渡動畫,你需要建立兩個 layout 佈局檔案來描述兩個不同場景的屬性。當從一個場景切換到另一個場景時,MotionLayout 框架會自動檢測這兩個場景中具有相同 idView 的屬性差別,然後針對這些差別屬性應用過渡動畫(類似於 TransitionManger)。

提示:不要將起始場景中某個 View 的可見性設為 GONE(即 andoird:visibility="gone"),如果設為 GONE,則該元素不會參與過渡動畫。

下面來看一個完整的例子,這個例子分為以下 3 步。

1 步:建立場景 1 的佈局檔案:

檔名:activity_main_scene1.xml

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/motionLayout"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
複製程式碼

場景 1 的佈局預覽如下圖所示:

MotionLayout 基礎教程

2 步:建立場景 2 的佈局檔案:

檔名:activity_main_scene2.xml

<?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"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
複製程式碼

場景 2 的佈局預覽如下圖所示:

MotionLayout 基礎教程

說明:場景 1 與場景 2 中都有一個 id 值為 imageImageView,它們的差別是:場景 1 中的 image 是水平垂直居中放置的,而場景 2 中的 image 是水平居中,垂直對齊到父佈局頂部的。因此當從場景 1 切換到場景 2 時,MotionLayout 將針對 image 的位置差別自動應用位移過渡動畫。

3 步:建立 MotionScene 檔案:

檔名:activity_main_motion_scene.xml,存放在 res/xml 目錄下

<?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:constraintSetStart="@layout/activity_main_scene1"
        app:constraintSetEnd="@layout/activity_main_scene2"
        app:duration="1000">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/image" />

    </Transition>

</MotionScene>
複製程式碼

編寫完 MotionLayout 檔案後就可以直接執行程式了。點選 image 即可進行場景切換。當進行場景切換時,MotionLayout 會自動計算出兩個場景之間的差別,然後應用相應的過渡動畫。

MotionLayout Demo

下面對 MotionLayout 檔案進行說明:

如上例所示,MotionScene 檔案的根元素是 <MotionScene>。在 <MotionScene> 元素中使用 <Transition> 子元素來描述一個過渡,使用 <Transition> 元素的 app:constraintSetStart 屬性指定起始場景的佈局檔案,使用 app:constraintSetEnd 指定結束場景的佈局檔案。在 <Transition> 元素中使用 <OnClick> 或者 <OnSwip> 子元素來描述過渡的觸發條件。

<Transition> 元素的屬性:

  • app:constraintSetStart:設定為起始場景的佈局檔案 Id
  • app:constraintSetEnd:設定為結束場景的佈局檔案 Id
  • app:duration:過渡動畫的持續時間。
  • app:motionInterpolator:過渡動畫的插值器。共有以下 6 個可選值:
    • linear:線性
    • easeIn:緩入
    • easeOut:緩出
    • easeInOut:緩入緩出
    • bounce:彈簧
    • anticipate:(功能未知,沒有找到文件)
  • app:staggered:【浮點型別】(功能未知,沒有找到文件)

可以在 <Transition> 元素中使用一個 <OnClick> 或者 <OnSwipe> 子元素來描述過渡的觸發條件。

<OnClick> 元素的屬性:

  • app:targetId:【id 值】設定用來觸發過渡的那個 ViewId(例如:@id/image@+id/image)。

提示app:targetId 的值的字首既可以是 @+id/ 也可以是 @id/,兩者都可以。官方示例中使用的是 @+id/。不過,使用 @id/ 字首似乎更加符合語義,因為 @+id/ 字首在佈局中常用來建立一個新的 Id,而 @id/ 字首則常用來引用其他的 Id 值。為了突出這裡引用的是其他的 Id 而不是新建了一個 Id,使用 @id/ 字首要更加符合語義。

  • app:clickAction:設定點選時執行的動作。該屬性共有以下 5 個可選的值:
    • toggle:在 Start 場景和 End 場景之間迴圈的切換。
    • transitionToEnd:過渡到 End 場景。
    • transitionToStart:過渡到 Start 場景。
    • jumpToEnd:跳到 End 場景(不執行過渡動畫)。
    • jumpToStart:跳到 Start 場景(不執行過渡動畫)。

<OnSwipe> 元素的屬性:

  • app:touchAnchorId:【id 值】設定需要追蹤的物件(例如:@id/image@+id/image)。
  • app:touchAnchorSide:設定需要追蹤你手指運動的物件邊界,共有以下 4 個可選值:
    • top
    • left
    • right
    • bottom
  • app:dragDirection:設定觸發過渡動畫的拖動方向。共有以下 4 個可選值:
    • dragUp:手指從下往上拖動(↑)。
    • dragDown:手指從上往下拖動(↓)。
    • dragLeft:手指從右往左拖動(←)。
    • dragRight:手指從左往右拖動(→)。
  • app:maxVelocity:【浮點值】設定動畫在拖動時的最大速度(單位:畫素每秒 px/s)。
  • app:maxAcceleration:【浮點值】設定動畫在拖動時的最大加速度(單位:畫素每二次方秒 ps/s^2)。

可以同時設定 <OnClick><OnSwipe> ,或者都不設定,而是使用程式碼來觸發過渡。

使用程式碼觸發過渡動畫

除了使用 <OnClick> 元素與 <OnSwipe> 元素來設定觸發過渡動畫的觸發條件外,還可以使用程式碼來手動觸發過渡動畫。

下面對場景 1 的佈局檔案進行修改,在佈局中新增 2 個按鈕,預覽如下圖所示:

MotionLayout 基礎教程

場景 1 修改後的佈局檔案內容為:

<?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"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnToStartScene"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="To Start Scene"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btnToEndScene" />

    <Button
        android:id="@+id/btnToEndScene"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="To End Scene"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/btnToStartScene"
        app:layout_constraintRight_toRightOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
複製程式碼

場景 2 的佈局檔案不需要修改。

MainActivity 中新增如下程式碼來手動執行過渡動畫:

public class MainActivity extends AppCompatActivity {
    private MotionLayout mMotionLayout;
    private Button btnToStartScene;
    private Button btnToEndScene;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_scene1);

        mMotionLayout = findViewById(R.id.motionLayout);
        btnToStartScene = findViewById(R.id.btnToStartScene);
        btnToEndScene = findViewById(R.id.btnToEndScene);

        btnToStartScene.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 切換到 Start 場景
                mMotionLayout.transitionToStart();
            }
        });

        btnToEndScene.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 切換到 End 場景
                mMotionLayout.transitionToEnd();
            }
        });
    }
}
複製程式碼

如上面程式碼中所示,呼叫 MotionLayouttransitionToStart() 方法可以切換到 Start 場景,呼叫 MotionLayouttransitionToStart() 方法可以切換到 End 場景。

效果如下所示:

MotionLayout 基礎教程

調整過渡動畫的進度

MotionLayout 還支援手動調整過渡動畫的播放進度。使用 MotionLayoutsetProgress(float pos) 方法(pos 引數的取值範圍為 [0.0 ~ 1.0])來調整過渡動畫的播放進度。

下面對場景 1 的佈局檔案進行修改,移除兩個按鈕,加入一個 SeekBar,修改後的佈局程式碼如下所示:

<?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"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="240dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="56dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
複製程式碼

佈局預覽如下圖所示:

MotionLayout 基礎教程

修改 MainActivity 中的程式碼:

public class MainActivity extends AppCompatActivity {
    private MotionLayout mMotionLayout;
    private SeekBar mSeekBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_scene1);

        mMotionLayout = findViewById(R.id.motionLayout);
        mSeekBar = findViewById(R.id.seekBar);

        mSeekBar.setMax(0);
        mSeekBar.setMax(100);
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                mMotionLayout.setProgress((float) (progress * 0.01));
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {

            }

            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {

            }
        });
    }
}
複製程式碼

效果如下圖所示:

MotionLayout 基礎教程

監聽 MotionLayout 過渡

可以呼叫 MotionLayoutsetTransitionListener() 方法向 MotionLayout 物件註冊一個過渡動畫監聽器,這個監聽器可以監聽過渡動畫的播放進度和結束事件。

public void setTransitionListener(MotionLayout.TransitionListener listener)
複製程式碼

TransitionListener 監聽器介面:

public interface TransitionListener {
    // 過渡動畫正在執行時呼叫
    void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress);
    // 過渡動畫結束時呼叫
    void onTransitionCompleted(MotionLayout motionLayout, int currentId);
}
複製程式碼

提示TransitionListener 介面在 alpha 版本中有所改動,可多出了 2 個回撥方法:onTransitionStartedonTransitionTrigger。由於 MotionLayout 還處於 alpha 版本,並未正式釋出,因此有所改動也是正常。

例:

MotionLayout motionLayout = findViewById(R.id.motionLayout);
motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
    @Override
    public void onTransitionChange(MotionLayout motionLayout, int i, int i1, float v) {
        Log.d("App", "onTransitionChange: " + v);
    }

    @Override
    public void onTransitionCompleted(MotionLayout motionLayout, int i) {
        Log.d("App", "onTransitionCompleted");
    }
});
複製程式碼

結語

MotionLayout 的基礎內容到此就介紹完畢了,你可能會覺得前面的例子不夠炫酷,因此這裡再給出一個更加炫酷的示例(這個例子很簡單,建議讀者自己嘗試一下):

MotionLayout Cool Demo

參考文章:


End

相關文章