MotionLayout 基礎教程
閱讀說明:
- 本文假設讀者已掌握
ConstraintLayout
的使用。 - 本文是一篇
MotionLayout
基礎教程,如您已掌握如何使用MotionLayout
,那麼本文可能對您幫助不大。 - 本文是基於
ConstraintLayout 2.0.0-alpha4
版本編寫的,建議與筆者的版本保持一致。 - 由於
MotionLayout
官方文件不全,有些知識點是根據筆者自己的理解總結的,如有錯誤,歡迎指正。
新增支援庫:
dependencies { ... implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4' } 複製程式碼
MotionLayout
最低支援到 Android 4.3(API 18)
,還有就是 MotionLayout
是 ConstraintLayout 2.0
新增的,因此必須確保支援庫的版本不低於 2.0
。
簡介
MotionLayout
類繼承自 ConstraintLayout
類,允許你為各種狀態之間的佈局設定過渡動畫。由於 MotionLayout
繼承了 ConstraintLayout
,因此可以直接在 XML
佈局檔案中使用 MotionLayout
替換 ConstraintLayout
。
MotionLayout
是完全宣告式的,你可以完全在 XML
檔案中描述一個複雜的過渡動畫而 無需任何程式碼 (如果您打算使用程式碼建立過渡動畫,那建議您優先使用屬性動畫,而不是 MotionLayout
)。
開始使用
由於 MotionLayout
類繼承自 ConstraintLayout
類,因此可以在佈局中使用 MotionLayout
替換掉 ConstraintLayout
。
MotionLayout
與 ConstraintLayout
不同的是, MotionLayout
需要連結到一個 MotionScene
檔案。使用 MotionLayout
的 app: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
框架會自動檢測這兩個場景中具有相同 id
的 View
的屬性差別,然後針對這些差別屬性應用過渡動畫(類似於 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
的佈局預覽如下圖所示:
第 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
的佈局預覽如下圖所示:
說明:場景 1
與場景 2
中都有一個 id
值為 image
的 ImageView
,它們的差別是:場景 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
檔案進行說明:
如上例所示, 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
值】設定用來觸發過渡的那個View
的Id
(例如:@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
個按鈕,預覽如下圖所示:
場景 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(); } }); } } 複製程式碼
如上面程式碼中所示,呼叫 MotionLayout
的 transitionToStart()
方法可以切換到 Start
場景,呼叫 MotionLayout
的 transitionToStart()
方法可以切換到 End
場景。
效果如下所示:
調整過渡動畫的進度
MotionLayout
還支援手動調整過渡動畫的播放進度。使用 MotionLayout
的 setProgress(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> 複製程式碼
佈局預覽如下圖所示:
修改 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
的 setTransitionListener()
方法向 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
個回撥方法: onTransitionStarted
和 onTransitionTrigger
。由於 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
的基礎內容到此就介紹完畢了,你可能會覺得前面的例子不夠炫酷,因此這裡再給出一個更加炫酷的示例(這個例子很簡單,建議讀者自己嘗試一下):