閱讀說明:
- 本文假設讀者已掌握
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
的基礎內容到此就介紹完畢了,你可能會覺得前面的例子不夠炫酷,因此這裡再給出一個更加炫酷的示例(這個例子很簡單,建議讀者自己嘗試一下):
參考文章: