閱讀說明:php
ConstraintLayout
。MotionLayout
基礎教程,如您已瞭解如何使用 MotionLayout
,本文可能對您幫助不大。ConstraintLayout 2.0.0-alpha4
版本編寫,建議讀者優先使用這一版本。MotionLayout
官方文檔不全,有些知識點是根據筆者本身的理解總結的,若有錯誤,歡迎指正。添加支持庫:html
dependencies {
...
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}
複製代碼
MotionLayout
最低支持到 Android 4.3(API 18)
,還有就是 MotionLayout
是 ConstraintLayout 2.0
添加的,所以必須確保支持庫的版本不低於 2.0
。java
MotionLayout
類繼承自 ConstraintLayout
類,容許你爲各類狀態之間的佈局設置過渡動畫。因爲 MotionLayout
繼承了 ConstraintLayout
,所以能夠直接在 XML
佈局文件中使用 MotionLayout
替換 ConstraintLayout
。android
MotionLayout
是徹底聲明式的,你能夠徹底在 XML
文件中描述一個複雜的過渡動畫而 無需任何代碼(若是您打算使用代碼建立過渡動畫,那建議您優先使用屬性動畫,而不是 MotionLayout
)。app
因爲 MotionLayout
類繼承自 ConstraintLayout
類,所以能夠在佈局中使用 MotionLayout
替換掉 ConstraintLayout
。框架
MotionLayout
與 ConstraintLayout
不一樣的是,MotionLayout
須要連接到一個 MotionScene
文件。使用 MotionLayout
的 app:layoutDescription
屬性將 MotionLayout
連接到一個 MotionScene
文件。ide
例:佈局
<?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
)。post
MotionScene
文件描述了兩個場景間的過渡動畫,存放在 res/xml
目錄下。gradle
要使用 MotionLayout
建立過渡動畫,你須要建立兩個 layout
佈局文件來描述兩個不一樣場景的屬性。當從一個場景切換到另外一個場景時,MotionLayout
框架會自動檢測這兩個場景中具備相同 id
的 View
的屬性差異,而後針對這些差異屬性應用過渡動畫(相似於 TransitionManger
)。
MotionLayout
框架支持的標準屬性:
android:visibility
android:alpha
android:elevation
android:rotation
android:rotationX
android:rotationY
android:scaleX
android:scaleY
android:translationX
android:translationY
android:translationZ
MationLayout
除了支持上面列出的標準屬性外,還支持所有的 ConstraintLayout
屬性。
下面來看一個完整的例子,這個例子分爲如下 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
值】設置拖動操做要關聯到的對象,讓觸摸操做看起來像是在拖動這個對象的由 app:touchAnchorSide
屬性指定的那個邊。app:touchAnchorSide
:設置觸摸操做將會拖動對象的哪一邊,共有如下 4
個可選值:
top
left
right
bottom
app:dragDirection
:設置拖動的方向(注意,只有設置了 app:touchAnchorId
屬性後該屬性纔有效)。共有如下 4
個可選值:
dragUp
:手指從下往上拖動(↑)。dragDown
:手指從上往下拖動(↓)。dragLeft
:手指從右往左拖動(←)。dragRight
:手指從左往右拖動(→)。app:maxVelocity
:【浮點值】設置動畫在拖動時的最大速度(單位:像素每秒 px/s
)。app:maxAcceleration
:【浮點值】設置動畫在拖動時的最大加速度(單位:像素每二次方秒 px/s^2
)。能夠同時設置 <OnClick>
與 <OnSwipe>
,或者都不設置,而是使用代碼來觸發過渡。
還能夠在 <Transition>
元素中設置多個 <OnClick>
,每一個 <OnClick>
均可以關聯到一個不一樣的控件上。雖然 <Transition>
元素中也能夠設置多個 <OnSwipe>
,可是後面的 <OnSwipe>
會替換掉前面的 <OnSwipe>
,最終使用的是最後一個 <OnSwipe>
。
<OnSwipe>
拖動操做因爲 <OnSwipe>
拖動操做涉及的交互較爲複雜,這裏單獨對它的如下 3
個屬性進行說明:
app:touchAnchorId
app:dragDirection
app:touchAnchorSide
首先是 app:touchAnchorId
屬性與 app:dragDirection
屬性。app:touchAnchorId
屬性用於設置拖動操做要關聯到的對象;app:dragDirection
屬性用於指定拖動方向。
默認狀況下,由上往下
拖動時會運行過渡動畫,此時 <OnSwipe/>
元素不須要設置任何屬性,只要在 <Transition>
中加一個 <OnSwipe/>
標籤便可。
例:
<Transition ...>
<OnSwipe/>
</Transition>
複製代碼
可是,若是你要支持 由下往上
(↑)或者 由左往右
(→)或者 由右往左
(←),那麼至少應該設置好 app:touchAnchorId
與 app:dragDirection
屬性。
app:dragDirection
屬性設置的拖動方向與 app:touchAnchorId
屬性關聯到的對象在 Start
場景和 End
場景中的位置是息息相關的。例以下圖 a
中,End
場景中的 Widget
位於 Start
場景中的 Widget
的上方,那麼應該設置 app:dragDirection="dragUp"
。再看圖 b
中,End
場景中的 Widget
位於 Start
場景中的 Widget
的右邊,那麼應該設置 app:dragDirection="dragRight"
:
若是 End
場景中的 Widget
相對於 Start
場景中的 Widget
是傾斜的(以下圖所示),將會有兩個可選的方向,下圖中的可選方向是 dragUp
、dragRight
,具體使用哪一個方向,由你本身決定。
設置一個正確的拖動方向是很是重要的,不然拖動時,過渡動畫將表現不佳。
提示:
MotionLayout
將使用app:touchAnchorId
關聯到的對象在app:dragDirection
方向上的拖動進度(progress
)做爲整個過渡動畫的進度,當關聯對象在app:dragDirection
方向上的拖動完成時,也就意味着整個過渡動畫完成了。
例:實現拖動效果
刪除 <Transition>
元素元素的 <OnClick>
標籤,並加入一個 <OnSwipe>
標籤,修改後的 MotionScene
文件內容以下所示:
<?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,加入 OnSwipe -->
<OnSwipe app:touchAnchorId="@id/image" app:dragDirection="dragUp"/>
</Transition>
</MotionScene>
複製代碼
注意:若是將 <OnClick>
和 <OnSwipe>
關聯到了同一個控件,或者 <OnSwipe>
關聯到的那個控件是可點擊的,點擊事件將會影響到拖動,你將沒法按住控件進行拖動,只能按住控件的外面才能拖動。
例:
在 <Transition>
標籤中加入 <OnClick>
,修改後的 MotionScene
文件內容以下所示:
<?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">
<OnSwipe app:touchAnchorId="@id/image" app:dragDirection="dragUp"/>
<!-- 加入 OnClick -->
<OnClick app:targetId="@id/image" app:clickAction="toggle"/>
</Transition>
</MotionScene>
複製代碼
效果以下所示:
app:touchAnchorSide
屬性:
app:touchAnchorSide
屬性的功能是 「設置觸摸操做將會拖動對象的哪一邊」,該屬性可用於實現可摺疊效果,例如可摺疊標題欄。
例:在底部實現一個向上拉的摺疊效果。
1. 修改 acticity_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" />
<!-- 增長如下代碼 -->
<FrameLayout android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="0dp" android:background="@color/colorPrimary" app:layout_constraintTop_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent">
<ImageView android:layout_gravity="center" android:src="@mipmap/ic_launcher" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</FrameLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
複製代碼
2. 修改 acticity_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" />
<!-- 增長如下代碼 -->
<FrameLayout android:id="@+id/bottomBar" android:layout_width="match_parent" android:layout_height="120dp" android:background="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent">
<ImageView android:layout_gravity="center" android:src="@mipmap/ic_launcher" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
</FrameLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>
複製代碼
3. 修改 MotionScene
文件(文件名:activity_main_motion_scene.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">
<!-- 關聯到 bottomBar 上-->
<OnSwipe app:touchAnchorId="@id/bottomBar" app:touchAnchorSide="top" app:dragDirection="dragUp"/>
<OnClick app:targetId="@id/image" app:clickAction="toggle"/>
</Transition>
</MotionScene>
複製代碼
效果以下所示:
提示:其實
<OnSwipe>
能夠不關聯到bottomBar
上,在由於在前面的例子中咱們已經把<OnSwipe>
關聯到了image
上,且拖動方向也設置正確(drageUp
),這樣其實已經能夠正常拖動了。可是因爲bottomBar
是可摺疊的,把<OnSwipe>
拖動關聯到它上面更加合適,這樣能夠設置app:touchAnchorSide="top"
,告訴MotionLayout
控件bottomBar
的上邊界是可拖動的,這樣更符合語義。
除了使用 <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
的 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");
}
});
複製代碼
本篇文章到此就結束了,你可能會以爲前面的例子不夠炫酷,這裏給出一個炫酷點的例子(這個例子很簡單,建議讀者動手嘗試實現一下):
後續文章:
參考文章: