距離上一篇文章「 MotionLayout:打開(kāi)動(dòng)畫新世界大門(part I)」已經(jīng)過(guò)去了很久未桥,由于個(gè)人原因巢掺,MotionLayout 系列文章姍姍來(lái)遲。在之前的文章中律想,我們領(lǐng)略到了 MotionLayout 的魅力逞壁,了解到它繼承自 ConstraintLayout流济,并具有它“約束布局”的特性。同時(shí)腌闯,關(guān)于如何創(chuàng)建和使用 MotionScene
及其內(nèi)部的 KeyFrameSet
也都做了一些簡(jiǎn)單介紹绳瘟。那么,本文來(lái)帶大家進(jìn)一步探索 KeyFrameSet
這個(gè)大家族中的“神秘寶藏”姿骏,并針對(duì)上文中留下的一些彩蛋進(jìn)行講解糖声,來(lái)看看如何實(shí)現(xiàn) MotionLayout 與其他控件的聯(lián)動(dòng)。
再探索 KeyFrameSet
在上文中我們說(shuō)到 KeyFrameSet 能夠讓單調(diào)的動(dòng)畫獨(dú)樹一幟分瘦,可以根據(jù)我們的意愿來(lái)描述動(dòng)畫運(yùn)動(dòng)的軌跡姨丈。之前只是比較詳細(xì)介紹了 KeyFrameSet 這個(gè)大家族中的 Keyposition,那么本文就來(lái)和大家窺探一下其他寶藏的秘密吧擅腰。
首先,我們來(lái)看一張熟悉的 MotionLayout 結(jié)構(gòu)圖:
從上圖我們可以看到翁潘,KeyFrameSet 中主要包含了KeyPosition趁冈、KeyAttribute 以及 KeyCycle 三種類型的關(guān)鍵幀。其實(shí)除此以外拜马,KeyFrameSet 還提供了 KeyTimeCycle 和 KeyTrigger渗勘,具體的用法和使用場(chǎng)景會(huì)在后續(xù)文章進(jìn)行介紹。本文中俩莽,我們先來(lái)詳細(xì)看一下 KeyAttribute 以及 KeyCycle旺坠。
KeyAttribute
我們知道,KeyPosition 描述的是目標(biāo) View 在某個(gè)位置的關(guān)鍵幀扮超,進(jìn)而改變動(dòng)畫的移動(dòng)軌跡取刃,至于 KeyAttribute蹋肮,則是描述這個(gè) View 在某個(gè)關(guān)鍵幀時(shí)所處的“狀態(tài)”,即所謂的”高矮胖瘦“璧疗。前者側(cè)重的是改變動(dòng)畫的軌跡坯辩,后者則是強(qiáng)調(diào)更改 View 自身的屬性。
從上圖的 KeyAttribute 結(jié)構(gòu)圖中我們可以看到崩侠,它支持各種屬性漆魔,足夠我們來(lái)描述一個(gè) View 的狀態(tài)了。假如我們希望實(shí)現(xiàn)如下效果:
其實(shí)上面的動(dòng)畫實(shí)現(xiàn)很簡(jiǎn)單却音,只需要在特定位置添加一些“關(guān)鍵幀”就可以了:
<?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="@+id/start"
app:constraintSetEnd="@+id/end"
app:duration="3200"
app:motionInterpolator="bounce">
<KeyFrameSet>
<KeyAttribute
app:motionTarget="@+id/loading_ball"
app:framePosition="20"
android:scaleX="1.5"
android:scaleY="1.5"
android:alpha="0.7"/>
<KeyAttribute
app:motionTarget="@+id/loading_ball"
app:framePosition="35"
android:scaleX="1"
android:scaleY="1"
android:alpha="1"/>
<KeyAttribute
app:motionTarget="@+id/loading_ball"
app:framePosition="50"
android:scaleX="1.5"
android:scaleY="1.5"
android:alpha="0.7"/>
<KeyAttribute
app:motionTarget="@+id/loading_ball"
app:framePosition="65"
android:scaleX="1"
android:scaleY="1"
android:alpha="1"/>
<KeyAttribute
app:motionTarget="@+id/loading_ball"
app:framePosition="80"
android:scaleX="1.5"
android:scaleY="1.5"
android:alpha="0.7"/>
<KeyAttribute
app:motionTarget="@+id/loading_ball"
app:framePosition="95"
android:scaleX="1"
android:scaleY="1"
android:alpha="1" />
</KeyFrameSet>
<OnClick app:targetId="@+id/loading_ball"
app:clickAction="toggle"/>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/loading_ball"
android:layout_width="32dp"
android:layout_height="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/loading_ball"
android:layout_width="32dp"
android:layout_height="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.85"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5" />
</ConstraintSet>
</MotionScene>
最終代碼如上所示改抡,是不是很 easy?這里我們?cè)谕窘?jīng)路線中添加一些特定的 keyAttribute
系瓢,并改變它們的屬性狀態(tài)阿纤,這里變化的屬性只涉及到 scaleX
、scaleY
和 alpha
八拱。
考慮到 KeyAttribute 中提供的屬性有限阵赠,所以,CustomAttribute 橫空出世肌稻,它支持任意自定義的屬性清蚀,常見(jiàn)的有 TextView
的 textColor
、background
或者是 ImageView
的 src
爹谭、tint
等枷邪。當(dāng)然還不止這些,我們平時(shí)自定義 View 中提供的自定義屬性同樣支持哦诺凡。就像 GitHub 上的一個(gè) ShapeOfView 的開(kāi)源項(xiàng)目东揣,可以提供給我們自定義控件形狀的功能,那么結(jié)合了 MotionLayout 中的 CustomAttribute
腹泌,我們就可以達(dá)到下面這種平滑轉(zhuǎn)換的效果:
舉個(gè)簡(jiǎn)單的例子嘶卧,上面的小球加載動(dòng)畫我們希望它能夠在運(yùn)動(dòng)過(guò)程中顏色也隨之變化,然而 <KeyAttribute> 中并沒(méi)有提供相關(guān)屬性凉袱,這里我們就可以借助于 <CustomAttribute> 來(lái)實(shí)現(xiàn)啦芥吟。改動(dòng)部分代碼如下所示:
......
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/loading_ball"
android:layout_width="32dp"
android:layout_height="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.15"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5">
<CustomAttribute
app:attributeName="colorFilter"
app:customColorValue="@android:color/holo_blue_light"/>
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/loading_ball"
android:layout_width="32dp"
android:layout_height="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.85"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5">
<CustomAttribute
app:attributeName="colorFilter"
app:customColorValue="@color/colorAccent"/>
</Constraint>
</ConstraintSet>
我們?cè)O(shè)置了 colorFilter
屬性,作用相當(dāng)于 tint
专甩,重新運(yùn)行后钟鸵,可以看到如下效果:
需要我們注意的是,這里的自定義屬性的 attributeName
對(duì)應(yīng)的值并不一定是在 xml 布局文件中控件對(duì)應(yīng)的屬性名稱涤躲,而是在對(duì)應(yīng)控件中擁有 setter 設(shè)置的屬性名稱棺耍。怎么理解呢?其實(shí)歸根結(jié)底 CustomAttribute 內(nèi)部還是利用的反射种樱,從下面的部分源碼中就能夠察覺(jué)到:
public void applyCustomAttributes(ConstraintLayout constraintLayout) {
int count = constraintLayout.getChildCount();
for(int i = 0; i < count; ++i) {
View view = constraintLayout.getChildAt(i);
int id = view.getId();
if (!this.mConstraints.containsKey(id)) {
Log.v("ConstraintSet", "id unknown " + Debug.getName(view));
} else {
if (this.mForceId && id == -1) {
throw new RuntimeException("All children of ConstraintLayout must have ids to use ConstraintSet");
}
if (this.mConstraints.containsKey(id)) {
ConstraintSet.Constraint constraint = (ConstraintSet.Constraint)this.mConstraints.get(id);
ConstraintAttribute.setAttributes(view, constraint.mCustomConstraints);
}
}
}
}
......
public static void setAttributes(View view, HashMap<String, ConstraintAttribute> map) {
Class<? extends View> viewClass = view.getClass();
Iterator var3 = map.keySet().iterator();
while(var3.hasNext()) {
String name = (String)var3.next();
ConstraintAttribute constraintAttribute = (ConstraintAttribute)map.get(name);
String methodName = "set" + name;
try {
Method method;
switch(constraintAttribute.mType) {
case COLOR_TYPE:
method = viewClass.getMethod(methodName, Integer.TYPE);
method.invoke(view, constraintAttribute.mColorValue);
break;
case COLOR_DRAWABLE_TYPE:
method = viewClass.getMethod(methodName, Drawable.class);
ColorDrawable drawable = new ColorDrawable();
drawable.setColor(constraintAttribute.mColorValue);
method.invoke(view, drawable);
break;
case INT_TYPE:
method = viewClass.getMethod(methodName, Integer.TYPE);
method.invoke(view, constraintAttribute.mIntegerValue);
break;
case FLOAT_TYPE:
method = viewClass.getMethod(methodName, Float.TYPE);
method.invoke(view, constraintAttribute.mFloatValue);
break;
case STRING_TYPE:
method = viewClass.getMethod(methodName, CharSequence.class);
method.invoke(view, constraintAttribute.mStringValue);
break;
case BOOLEAN_TYPE:
method = viewClass.getMethod(methodName, Boolean.TYPE);
method.invoke(view, constraintAttribute.mBooleanValue);
break;
case DIMENSION_TYPE:
method = viewClass.getMethod(methodName, Float.TYPE);
method.invoke(view, constraintAttribute.mFloatValue);
}
} catch (NoSuchMethodException var9) {
Log.e("TransitionLayout", var9.getMessage());
Log.e("TransitionLayout", " Custom Attribute \"" + name + "\" not found on " + viewClass.getName());
Log.e("TransitionLayout", viewClass.getName() + " must have a method " + methodName);
} catch (IllegalAccessException var10) {
Log.e("TransitionLayout", " Custom Attribute \"" + name + "\" not found on " + viewClass.getName());
var10.printStackTrace();
} catch (InvocationTargetException var11) {
Log.e("TransitionLayout", " Custom Attribute \"" + name + "\" not found on " + viewClass.getName());
var11.printStackTrace();
}
}
}
首先在 MotionLayout 中蒙袍,如果是自定義屬性俊卤,那么會(huì)執(zhí)行 ConstraintSet 類中的 applyCustomAttributes
方法,接著會(huì)調(diào)用 ConstraintAttribute 類中的 setAttributes
方法左敌,就如上代碼中所寫的那樣瘾蛋,它會(huì)根據(jù)屬性名稱組裝成對(duì)應(yīng)的 set 方法,然后通過(guò)反射調(diào)用矫限。是不是有種恍然大悟的感覺(jué)哺哼?話說(shuō),這樣的機(jī)制是不是好像哪里見(jiàn)到過(guò)叼风?沒(méi)錯(cuò)取董,正是屬性動(dòng)畫。
KeyCycle
什么是 KeyCycle 呢无宿?下面是來(lái)自 Gal Maoz 的總結(jié):
A
KeyCycle
is a highly-detailed, custom-made interpolator for a specific view, whereas the interpolator is influencing the entire scene, with a large focus on repetitive actions (hence the cycle in the name).
簡(jiǎn)單來(lái)說(shuō)茵汰,KeyCycle 是針對(duì)特定視圖的非常詳細(xì)的定制化插值器。它比較適合我們常說(shuō)的波形或周期運(yùn)動(dòng)場(chǎng)景孽鸡,比如實(shí)現(xiàn)控件的抖動(dòng)動(dòng)畫或者周期性的循環(huán)動(dòng)畫蹂午。
如上圖所示,KeyCycle
主要由以上幾個(gè)屬性組成彬碱,前兩個(gè)相信大家都比較熟悉了豆胸,這里不必多說(shuō),另外 view properties
正如之前的 KeyAttribute
結(jié)構(gòu)圖中所描述的那樣巷疼,代表View的各種屬性晚胡,如 rotation、translation嚼沿、alpha 等等估盘。 這里主要介紹另外三個(gè)比較重要且具有特色的屬性:
-
wavePeriod
:這個(gè)表示在當(dāng)前場(chǎng)景位置下需要執(zhí)行動(dòng)畫的波(周期)的數(shù)量。這樣說(shuō)可能不太容易理解骡尽,別急遣妥,我們待會(huì)舉個(gè)例子說(shuō)明。 -
waveOffset
:表示當(dāng)前控件需要變化的屬性的偏移量攀细,即 view properties 所對(duì)應(yīng)的初始值或者基準(zhǔn)值箫踩。例如,如果我們?cè)趧?dòng)畫執(zhí)行的某個(gè)位置設(shè)置了scaleX
為 0.3辨图,而設(shè)置了waveOffset
值為 1,那么肢藐,動(dòng)畫執(zhí)行到該位置故河,控件的實(shí)際寬度會(huì)變?yōu)?1 + 0.3 = 1.3
,也就是會(huì)擴(kuò)大為 1.3 倍吆豹,而不是縮小為之前的 0.3 倍鱼的。 -
waveShape
:這個(gè)屬性比較好理解理盆,即波的形狀,常見(jiàn)的值有:sin凑阶、cos猿规、sawtooth 等,更多可參考官網(wǎng)API:https://developer.android.com/reference/androidx/constraintlayout/motion/widget/MotionLayout#keycycle
下面舉個(gè)簡(jiǎn)單的例子幫助理解宙橱,以下面這個(gè)效果為例:
對(duì)應(yīng)的 KeyFrameSet
代碼如下所示:
<KeyFrameSet>
<KeyCycle
motion:framePosition="0"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="1"
motion:waveShape="sin"
android:scaleX="0.3"/>
<KeyCycle
motion:framePosition="18"
motion:target="@+id/button"
motion:wavePeriod="0"
motion:waveOffset="1"
motion:waveShape="sin"
android:scaleX="0.3"/>
<KeyCycle
motion:framePosition="100"
motion:target="@+id/button"
motion:wavePeriod="3"
motion:waveOffset="1"
motion:waveShape="sin"
android:scaleX="0"/>
</KeyFrameSet>
根據(jù)動(dòng)畫效果結(jié)合代碼可以知道姨俩,我們這個(gè)放大的Q彈的效果只是改變了 scaleX
這個(gè)屬性,并且讓它“搖擺了”大概三個(gè)來(lái)回(周期)师郑,恰好 wavePeriod
屬性值為 3环葵。也許動(dòng)畫不太方便察覺(jué),這樣宝冕,我們借助于 Google 提供的專門用來(lái)查看 KeyCycle 波形變化的快捷工具來(lái)查看它波形變化過(guò)程:
如此一來(lái)张遭,我們就很直觀地看到上圖中描繪的波形變化過(guò)程了,的確是三個(gè)周期沒(méi)有錯(cuò)地梨,并且是以正弦 sin 來(lái)變化的菊卷。
關(guān)于這款工具的使用,大家可以前往:https://github.com/googlearchive/android-ConstraintLayoutExamples/releases/download/1.0/CycleEditor.jar 上下載宝剖,然后通過(guò)執(zhí)行 java -jar [xx/CycleEditor.jar]
即可看到可視化界面洁闰,然后將 KeyFrameSet 部分的代碼 copy 到編輯欄,然后點(diǎn)擊 File -> parse xml 即可看到代碼對(duì)應(yīng)的波形走勢(shì)诈闺。如下所示:
我們來(lái)看看下面這個(gè)效果:
這個(gè)Q彈的效果就是基于 KeyCycle 實(shí)現(xiàn)的渴庆,我們來(lái)看看它的場(chǎng)景實(shí)現(xiàn):
<?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="@+id/start"
app:constraintSetEnd="@+id/end"
app:motionInterpolator="easeInOut"
app:duration="5200">
<KeyFrameSet>
<KeyCycle
app:motionTarget="@+id/image"
app:framePosition="10"
android:rotationY="22"
app:wavePeriod="2"
app:waveShape="sin"
app:waveOffset="1"/>
<KeyCycle
app:motionTarget="@+id/image"
app:framePosition="30"
android:rotationX="15"
app:wavePeriod="1"
app:waveShape="sin"
app:waveOffset="0"/>
<KeyCycle
app:motionTarget="@+id/image"
app:framePosition="65"
android:rotationY="14"
app:wavePeriod="1"
app:waveShape="sin"
app:waveOffset="0"/>
<KeyCycle
app:motionTarget="@+id/image"
app:framePosition="92"
android:rotationY="0"
android:rotationX="2"
app:wavePeriod="0"
app:waveShape="sin"
app:waveOffset="0"/>
</KeyFrameSet>
<OnClick app:targetId="@+id/image"
app:clickAction="toggle"/>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/image"
android:layout_width="120dp"
android:layout_height="120dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.76"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/image"
android:layout_width="120dp"
android:layout_height="120dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.76"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.45"/>
</ConstraintSet>
</MotionScene>
我們?cè)趧?dòng)畫路徑上添加一些關(guān)鍵幀,并稍微改變控件的旋轉(zhuǎn)角度雅镊,配合 keyCycle 就能達(dá)到上面的彈性動(dòng)畫襟雷,大家可以自己動(dòng)手嘗試體驗(yàn)一下。
MotionLayout 的聯(lián)動(dòng)性
很多時(shí)候仁烹,我們的控件并不只是單一的個(gè)體耸弄,而是需要與其他控件產(chǎn)生“交互上的關(guān)聯(lián)”,常見(jiàn)地卓缰,Android 的Material design components 全家桶中提供了一套“優(yōu)雅靈動(dòng)”的組件计呈,相信大家都體驗(yàn)過(guò)了,那么征唬,我們的 MotionLayout 可以與它們碰撞出怎樣的火花呢捌显?
一切從“頭”開(kāi)始
Material design 組件庫(kù)中提供了一個(gè) AppBarLayout 組件,我們經(jīng)常使用它來(lái)配合 CoordinatorLayout 控件實(shí)現(xiàn)一些簡(jiǎn)單的交互動(dòng)作总寒,例如頭部導(dǎo)航欄的伸縮效果扶歪,各位應(yīng)該或多或少都用到過(guò),這里不再介紹摄闸。下面我們就從 AppBarLayout 開(kāi)始善镰,看看如何實(shí)現(xiàn)與 MotionLayout 的聯(lián)動(dòng)妹萨。首先,我們先來(lái)看下面這個(gè)簡(jiǎn)單的效果:
我們知道炫欺,通過(guò) CoordinatorLayout
和 AppBarLayout
也可以實(shí)現(xiàn)類似的交互效果乎完,但顯然 MotionLayout 會(huì)更加靈活多變。其實(shí)上面的動(dòng)畫效果很簡(jiǎn)單品洛,只是在 AppBarLayout 高度變化過(guò)程中改變背景色树姨、標(biāo)題的位置和大小即可,對(duì)應(yīng)的 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"
xmlns:motion="http://schemas.android.com/tools">
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:layout_constraintBottom_toBottomOf="parent">
<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@color/blue_magic"/>
</Constraint>
<Constraint
android:id="@+id/tipText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleY="1.6"
android:scaleX="1.6"
android:alpha="1.0"
android:layout_marginStart="62dp"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:layout_constraintBottom_toBottomOf="parent">
<CustomAttribute
app:attributeName="backgroundColor"
app:customColorValue="@color/bgColor_dark"/>
</Constraint>
<Constraint
android:id="@id/tipText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:layout_marginBottom="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</ConstraintSet>
<Transition
app:constraintSetStart="@id/start"
app:constraintSetEnd="@id/end"
app:duration="4000">
<KeyFrameSet>
<KeyPosition
app:framePosition="60"
app:motionTarget="@id/tipText"
app:keyPositionType="parentRelative"
app:percentY="0.7"/>
</KeyFrameSet>
</Transition>
</MotionScene>
結(jié)合以上效果圖毫别,我們很容易理解上面的場(chǎng)景實(shí)現(xiàn)代碼娃弓,那么,我們?cè)賮?lái)看下布局文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false"
android:background="@android:color/white"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="260dp"
android:theme="@style/AppTheme.AppBarOverlay">
<com.moos.constraint.widget.MotionToolBar
android:id="@+id/motionLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:motionDebug="NO_DEBUG"
app:layoutDescription="@xml/motion_scene_simple_appbar"
android:minHeight="52dp"
app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed">
<View
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/blue_magic" />
<TextView
android:id="@+id/tipText"
android:text="Time flies fast"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textColor="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</com.moos.constraint.widget.MotionToolBar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/content_text_color"
android:lineSpacingExtra="8dp"
android:padding="12dp"
android:text="@string/long_text_en"/>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
觀察上面布局文件岛宦,其實(shí)代碼與傳統(tǒng) CoordinatorLayout & AppBarLayout 交互的代碼大同小異台丛,只不過(guò)我們?cè)?AppBarLayout
內(nèi)部添加了一個(gè) MotionToolBar 控件,這其實(shí)是個(gè) MotionLayout
砾肺,只不過(guò)內(nèi)部根據(jù) AppBarLayout
伸縮的高度動(dòng)態(tài)改變動(dòng)畫進(jìn)度而已挽霉,我們來(lái)看下具體實(shí)現(xiàn):
class MotionToolBar @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), AppBarLayout.OnOffsetChangedListener {
override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
Log.e("MotionToolBar", "onOffsetChanged: ----->$verticalOffset, scroll range--> ${appBarLayout?.totalScrollRange}")
val seekPosition = -verticalOffset / (appBarLayout?.totalScrollRange!!.toFloat()/5*3)
progress = seekPosition
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
(parent as? AppBarLayout)?.addOnOffsetChangedListener(this)
}
}
代碼量很少,通過(guò)在 onOffsetChanged
方法中監(jiān)聽(tīng) AppBarLayout
的伸縮高度变汪,并經(jīng)過(guò)換算后得到當(dāng)前的進(jìn)度值傳遞給 progress
侠坎,該字段就對(duì)應(yīng)著 MotionLayout 的 setProgress
方法,如此一來(lái)就能夠動(dòng)態(tài)的改變其動(dòng)畫進(jìn)度了裙盾。
理解了上述代碼实胸,就不難實(shí)現(xiàn)下面的效果了:
具體代碼就不貼了,文末會(huì)附上 GitHub 倉(cāng)庫(kù)地址番官,所有效果實(shí)現(xiàn)代碼都能夠在里面找到庐完。
Lottie 與 MotionLayout 的雙劍合璧
Lottie 想必大家都了解過(guò),它是一個(gè)動(dòng)畫工具徘熔,能夠?qū)?UI 的設(shè)計(jì)動(dòng)畫效果轉(zhuǎn)為 Json 格式的數(shù)據(jù)文件门躯,然后各端都提供了相應(yīng)的庫(kù)來(lái)解析并執(zhí)行動(dòng)畫文件,很多時(shí)候需要花費(fèi)大量時(shí)間去借助于代碼實(shí)現(xiàn)的復(fù)雜動(dòng)畫酷师,如今不費(fèi)吹灰之力就搞定了讶凉,很大程度上解放了我們的雙手。
那么山孔,Lottie 與 MotionLayout 一起能夠碰撞出怎樣的火花呢懂讯?我們以下面的一個(gè)簡(jiǎn)單效果為例:
其實(shí)簡(jiǎn)單來(lái)說(shuō),MotionLayout 能夠?qū)⒆陨淼膭?dòng)畫過(guò)程與 Lottie 同步台颠,就像圖中的安卓機(jī)器人動(dòng)畫就是 MotionLayout 實(shí)現(xiàn)的褐望,而下面的卡通人物眼神游離的動(dòng)畫則是 Lottie 動(dòng)畫,從圖中可以看到,通過(guò)手勢(shì)滑動(dòng) ViewPager
兩個(gè)動(dòng)畫一直保持著“同步運(yùn)動(dòng)”譬挚。下面我們來(lái)看看如何實(shí)現(xiàn)的,首先是布局文件酪呻,比較簡(jiǎn)單:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/motionView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.moos.constraint.widget.ViewpagerHeader
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="300dp"
app:layoutDescription="@xml/motion_with_view_pager"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:motionProgress="0">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:lottie_rawRes="@raw/face"/>
<ImageView
android:id="@+id/ic_robot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_foreground"/>
</com.moos.constraint.widget.ViewpagerHeader>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabSelectedTextColor="@color/colorAccent"
app:tabTextColor="@color/content_text_color"
app:layout_constraintTop_toBottomOf="@+id/header"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@+id/tabLayout"
app:layout_constraintBottom_toBottomOf="parent">
</androidx.viewpager.widget.ViewPager>
</androidx.constraintlayout.widget.ConstraintLayout>
至于這個(gè) ViewPagerHeader 相信大家也猜到了减宣,其實(shí)也是個(gè) MotionLayout :
class ViewpagerHeader @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), androidx.viewpager.widget.ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
val animateProgress = (position.toFloat() + positionOffset)/3
Log.e("LottieMotionActivity", "viewpager scroll progress is: $animateProgress")
progress = animateProgress
}
override fun onPageSelected(position: Int) {
}
}
只不過(guò)它內(nèi)部實(shí)現(xiàn)了 ViewPager
的 onPageChangeListener
,以監(jiān)聽(tīng)頁(yè)面的滑動(dòng)狀態(tài)玩荠,然后計(jì)算出此時(shí) MotionLayout 的動(dòng)畫進(jìn)度漆腌,這里由于 json 動(dòng)畫文件存在問(wèn)題,所以只截取了一部分動(dòng)畫過(guò)程來(lái)執(zhí)行阶冈。說(shuō)了這么多闷尿,它的 MotionScene
是什么樣的呢?其實(shí)很 easy:
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Transition
motion:constraintSetStart="@+id/start"
motion:constraintSetEnd="@+id/end">
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/lottieView"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:progress="0"/>
<Constraint
android:id="@id/ic_robot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintStart_toStartOf="parent">
<CustomAttribute
app:attributeName="colorFilter"
app:customColorValue="@android:color/holo_blue_light"/>
</Constraint>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/lottieView"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:progress="1"/>
<Constraint
android:id="@id/ic_robot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintEnd_toEndOf="parent">
<CustomAttribute
app:attributeName="colorFilter"
app:customColorValue="@color/colorAccent"/>
</Constraint>
</ConstraintSet>
</MotionScene>
唯一需要值得注意的是:這里我們分別在 MotionLayout 的起始位置和終止位置設(shè)置了 motion:progress
屬性為 0 和 1女坑,由于 LottieAnimationView
內(nèi)部擁有 setProgress
方法填具,這樣做的目的就是將 Lottie 的動(dòng)畫過(guò)程與 MotionLayout 進(jìn)行綁定,我們只需要改變這個(gè)屬性匆骗,就能夠間接控制 Lottie 動(dòng)畫啦劳景。
最后,我們只需要在 Activity 中設(shè)置如下代碼就可以成功執(zhí)行啦:
val adapter = ViewPagerAdapter(supportFragmentManager)
adapter.addPage("Now", R.layout.holder_layout)
adapter.addPage("Discover", R.layout.holder_layout)
viewPager.adapter = adapter
tabLayout.setupWithViewPager(viewPager)
viewPager.addOnPageChangeListener(header as androidx.viewpager.widget.ViewPager.OnPageChangeListener)
當(dāng)然碉就,MotionLayout 還能和很多組件進(jìn)行聯(lián)動(dòng)盟广,篇幅有限就不一一介紹啦,到這里瓮钥,我們本篇文章內(nèi)容也差不多該告一段落了筋量,關(guān)于 MotionLayout 系列文章的所有示例代碼都能夠在 GitHub 倉(cāng)庫(kù)中找到:
后續(xù)
如此一來(lái),MotionLayout 系列已經(jīng)完成兩篇文章了碉熄,剩下的內(nèi)容應(yīng)該還需要一篇文章來(lái)容納桨武,后續(xù)可能還會(huì)額外提供一篇實(shí)戰(zhàn)系列文章。下一篇文章主要介紹 KeyFrameSet 家族最后一個(gè)成員以及 MotionLayout 多狀態(tài)場(chǎng)景的使用具被,同時(shí)玻募,也會(huì)介紹如何實(shí)現(xiàn)與 RecyclerView “強(qiáng)強(qiáng)聯(lián)合”。最后一姿,Google 在 Android studio 4.2 終于推出了 Motion Editor 工具七咧,下篇文章也會(huì)通過(guò)一個(gè)小實(shí)戰(zhàn)項(xiàng)目來(lái)介紹其用法,拭目以待叮叹。
筆者說(shuō)
最近這兩篇文章都盡量做到每個(gè)重要知識(shí)點(diǎn)都提供一個(gè)實(shí)戰(zhàn)的小示例艾栋,力求做到加深理解,文中很多內(nèi)容都參考自 Nicolas Roard 對(duì)于 MotionLayout 的系列教程和 Android 官方文檔蛉顽,并加入自己的理解蝗砾。從去年編撰第一篇文章時(shí)來(lái)看,國(guó)內(nèi)對(duì)于 MotionLayout 的系列文章非常少,寫文章的目的其實(shí)很簡(jiǎn)單悼粮,讓自己消化新知識(shí)的同時(shí)闲勺,也能夠讓更多國(guó)人知道、認(rèn)識(shí)和嘗試使用 MotionLayout 這個(gè)全新的動(dòng)畫組件扣猫。
由于個(gè)人技術(shù)能力和表述能力有限菜循,很多內(nèi)容可能并沒(méi)有講解全面和透徹,如果有什么建議或者問(wèn)題申尤,歡迎留言區(qū)探討癌幕,一起進(jìn)步。