用 MotionLayout 來做過渡動(dòng)畫

題圖

MotionLayout 是一個(gè) Google 官方出品用于制作 Android 中的過渡動(dòng)畫的框架。用來它就能輕松的做出一些較為復(fù)雜的動(dòng)畫效果。

由于 MotionLayout 是基于 ConstraintLayout ,所以其中涉及到了部分關(guān)于 ConstraintLayout 的基本知識(shí),本文按下不表,對(duì) ConstraintLayout 不熟悉的同學(xué),可以查看鴻洋的這篇博客。

MotionLayout 是 ConstraintLayout 的子類,并且在 ConstraintLayout 發(fā)展到 2.0 時(shí)才加入 ConstraintLayout 這個(gè)庫,本文所使用的依賴為:

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'

接下來讓我們進(jìn)入正題,先來看看我用 MotionLayout 制作的一個(gè) Demo。

image

在這個(gè)例子中,當(dāng)點(diǎn)擊 Login 按鈕時(shí),Login 按鈕的長度進(jìn)行不斷縮小,縮小到一定尺寸時(shí),外層的 ProgressBar 還是逐漸由不可見變?yōu)榭梢?,同時(shí),Login 按鈕上的字進(jìn)行了淡入淡出的動(dòng)畫效果。

MotionLayout 能做的不僅如此,它還能做到其他更為好玩有趣的過渡動(dòng)畫。現(xiàn)在讓我們來學(xué)一下吧。

過渡動(dòng)畫,顧名思義就是在狀態(tài)之間進(jìn)行過渡的動(dòng)畫效果,防止頁面內(nèi) View 出現(xiàn)瞬間移動(dòng)的效果。而 MotionLayout 的重點(diǎn)其實(shí)就是狀態(tài)。開發(fā)者只需要定義好對(duì)應(yīng)狀態(tài)下 View 的相對(duì)位置,以及相關(guān)屬性,其后 MotionLayout 便會(huì)自動(dòng)為其增加動(dòng)的效果。

image

這樣的一個(gè)最簡(jiǎn)單的效果是怎么做出來的呢?

首先我們需要在資源文件夾 res 下新建一個(gè)名為 xml 的資源文件夾,然后再 xml 文件夾內(nèi)新建一個(gè)根節(jié)點(diǎn)是 MotionScene 的 xml 文件,demo 中這個(gè) xml 的文件名為 login_animator。

以下就是實(shí)現(xiàn) Login 按鈕長度變換的過渡動(dòng)畫。

<?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:constraintSetEnd="@id/a_login_end"
        app:constraintSetStart="@id/a_login_start"
        app:duration="1000">
        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/tv_action_login" />

    </Transition>

    <ConstraintSet android:id="@+id/a_login_start">
        <Constraint android:id="@+id/tv_action_login">
            <Layout
                android:layout_width="match_parent"
                android:layout_height="48dp"
                                android:layout_marginTop="30dp"
                android:layout_marginStart="30dp"
                android:layout_marginEnd="30dp"
                app:layout_constraintTop_toBottomOf="@id/et_passwd" />
        </Constraint>

    </ConstraintSet>

    <ConstraintSet android:id="@+id/a_login_end">
        <Constraint android:id="@+id/tv_action_login">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_marginTop="30dp"
                app:layout_constraintEnd_toEndOf="@+id/et_account"
                app:layout_constraintStart_toStartOf="@+id/et_account"
                app:layout_constraintTop_toBottomOf="@id/et_passwd" />
        </Constraint>

    </ConstraintSet>

</MotionScene>

仔細(xì)看其中的信息,其中大部分是我們都熟悉的,無非就是對(duì) View 的相對(duì)位置的約定或是 View 自身屬性的規(guī)定,少部分是關(guān)于過渡動(dòng)畫的。

我們先來看看這個(gè)文件的整體結(jié)構(gòu),首先根節(jié)點(diǎn)是 MotionScene ,MotionScene 節(jié)點(diǎn)下有一個(gè) Transition 與兩個(gè) ConstraintSet 節(jié)點(diǎn),而且 Transition 中有兩個(gè)屬性,一個(gè)是 constraintSetStart 另一個(gè)是 constraintSetEnd,這兩個(gè)屬性的值正好是兩個(gè) ConstraintSet 節(jié)點(diǎn)的 id,而 Transition 內(nèi)子節(jié)點(diǎn) OnClick 節(jié)點(diǎn)內(nèi)的屬性 targetId 則表明了當(dāng)前 Transition 所指定的動(dòng)畫是作用于具體的 View 上。

如你所想,通過在 Transition 內(nèi)指定某個(gè) View 的兩個(gè)狀態(tài)下的不同屬性,就能產(chǎn)生在這兩個(gè)狀態(tài)內(nèi)的過渡動(dòng)畫,并且在 Translation 內(nèi)通過組合不同的動(dòng)畫事件進(jìn)行顯示。比如點(diǎn)擊產(chǎn)生的動(dòng)畫(OnClick),滑動(dòng)產(chǎn)生的動(dòng)畫(OnSwipe),以及可改變某一幀動(dòng)畫效果的關(guān)鍵幀動(dòng)畫(KeyFrameSet)。

當(dāng)我們把初始及結(jié)束狀態(tài)下的屬性及動(dòng)畫定義完成后,還需要回到我們的布局文件,將需要實(shí)現(xiàn)過渡動(dòng)畫的 View 的父布局改為 MotionLayout 并且給它添加一個(gè)值為剛才我們新建那個(gè) xml 文件的引用的屬性 layoutDescription。

<?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/login_animator">
    ......
</androidx.constraintlayout.motion.widget.MotionLayout>

這就是一個(gè)最為簡(jiǎn)單的使用 MotionLayout 實(shí)現(xiàn)過渡動(dòng)畫的例子,它與開頭我自己寫的那個(gè) demo 沒什么差別,無非就是 demo 中變換的 View 的個(gè)數(shù)及屬性多少不同而已。

在這個(gè)例子中,我們通過在 Transition 中定義了一個(gè) OnClick 的子節(jié)點(diǎn),而達(dá)到點(diǎn)擊產(chǎn)生動(dòng)畫的效果。其中,targetId 即為產(chǎn)生動(dòng)畫效果的目標(biāo) View 的 id;clickAction 則是指明在是在開始或是再結(jié)束狀態(tài)時(shí)產(chǎn)生動(dòng)畫,toggle 表示在開始和結(jié)束狀態(tài)時(shí)均有效,它還有 transitionToStart 和 transitionToEnd 表示只在開始或是結(jié)束狀態(tài)下有效。有興趣的可以去試試。

除了 OnClick,我們還可以在 Translation 中定義 OnSwipe 節(jié)點(diǎn),OnSwipe 就是用來處理屏幕上的滑動(dòng)事件,以此配合指定的 View 實(shí)現(xiàn)過渡動(dòng)畫的效果。

image

給 MotionLayout 添加 motionDebug="SHOW_PATH" 這個(gè)屬性,即可查看 View 的過渡動(dòng)畫的軌跡。

通過指定 View 的開始狀態(tài)(靠近屏幕左邊)和結(jié)束狀態(tài)(靠近屏幕右邊),然后在 Translation 中聲明出滑動(dòng)事件,即可。

<?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:constraintSetEnd="@id/v_swipe_end"
        app:constraintSetStart="@id/v_swipe_start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragRight"
            app:touchRegionId="@id/v_swipe" />

    </Transition>
    
    <ConstraintSet android:id="@+id/v_swipe_start">
        <Constraint android:id="@+id/v_swipe">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>

    </ConstraintSet>

    <ConstraintSet android:id="@+id/v_swipe_end">
        <Constraint android:id="@+id/v_swipe">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>

    </ConstraintSet>

</MotionScene>

在 OnSwipe 中,有兩個(gè)屬性,一個(gè)是 dragDirection 代表的是滑動(dòng)的方向,touchRegionId 則指明了監(jiān)聽的滑動(dòng)區(qū)域?yàn)?View 的滑動(dòng)區(qū)域。既然能作用于 View 的滑動(dòng)區(qū)域,是不是也能作用于整個(gè)屏幕的滑動(dòng)區(qū)域呢?沒錯(cuò),touchAnchorId 則表示全部的滑動(dòng)區(qū)域。OnSwipe 還有一些其他屬性,比如:touchAnchorSide 表示監(jiān)聽 View 的哪個(gè)區(qū)域的滑動(dòng)監(jiān)聽,如果不設(shè)置的話,是 View 外的所有區(qū)域;onTouchUp 表示當(dāng)在滑動(dòng)過程中手指抬起時(shí)動(dòng)畫的動(dòng)作(回到開始狀態(tài)、回到結(jié)束狀態(tài)、自動(dòng)完成、停止等等)。

說實(shí)話,我在開始嘗試 MotionLayout 的時(shí)候被 OnSwipe 給嚇到了,但是當(dāng)我更進(jìn)一步的使用 KeyFrameSet 的時(shí)候直喊 666。原因就是因?yàn)?KeyFrameSet 能做出更炫酷的效果。

KeyFrameSet 是作用于在過渡動(dòng)畫過程中的關(guān)鍵幀,通過指定動(dòng)畫關(guān)鍵進(jìn)程時(shí)的狀態(tài)來實(shí)現(xiàn)不同的效果。舉個(gè)例子,當(dāng)前的 View 滑動(dòng)是一條直線,我想讓在滑動(dòng)過程中有一個(gè)先向上滑動(dòng),然后向下滑動(dòng)以這種效果達(dá)到屏幕的最右側(cè)。

image

View 的開始與結(jié)束狀態(tài)沒有發(fā)生改變,只是在過渡動(dòng)畫的中點(diǎn)區(qū)域進(jìn)行改變 View 的坐標(biāo)。

<Transition
    app:constraintSetEnd="@id/v_swipe_end"
    app:constraintSetStart="@id/v_swipe_start"
    app:duration="1000">
    <OnSwipe
        app:dragDirection="dragRight"
        app:touchAnchorId="@id/v_swipe"
        app:touchAnchorSide="bottom" />

    <KeyFrameSet>
        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="parentRelative"
            app:motionTarget="@+id/v_swipe"
            app:percentY="0.3" />
    </KeyFrameSet>

</Transition>

framePosition 表示在運(yùn)動(dòng)到整個(gè)運(yùn)動(dòng)過程的 50% 處,這個(gè)值的取值范圍是 0 - 100,motionTarget 表示作用的 View,而 keyPositionType 與percentY 則共同決定了運(yùn)動(dòng)軌跡中弧度的變化方向。keyPositionType 控制 percentY 的坐標(biāo)系的工作方式,它一共有 3 個(gè)值。parentRelative、deltaRelative、pathRelative。percentY 取值范圍為 0 - 1,同時(shí)允許負(fù)數(shù)及大于 1 的值。

parentRelative 表示,坐標(biāo)按照父布局的坐標(biāo)進(jìn)行處理,X,Y 軸的最大值均為1,X 軸向右為正,向左為負(fù),Y 軸向下為正,向上為負(fù)。

image

deltaRelative 表示開始狀態(tài)的中心點(diǎn)為坐標(biāo)系原點(diǎn),X,Y 軸的最大值均為1,X 軸向右為正,向左為負(fù),Y 軸向下為負(fù),向上為正。

image

pathRelative 表示開始狀態(tài)的中心點(diǎn)為坐標(biāo)系原點(diǎn),X 軸為兩個(gè)狀中心點(diǎn)的構(gòu)成的直線。X,Y 軸的最大值均為1,X 軸向結(jié)束狀態(tài)方向?yàn)檎蜷_始狀態(tài)方向?yàn)樨?fù),Y 軸向下為負(fù),向上為正。

image

keyPositionType 三個(gè)屬性的描述圖均來自 CodeLab

KeyPostition 還有些其他有趣的屬性,比如,控制運(yùn)動(dòng)軌跡是平滑的曲線還是直線的 curveFit,以及 transitionEasing 控制運(yùn)動(dòng)過程的加速或是減速等等。這里就不一一舉例了。

而且,還可以同時(shí)存在多個(gè)關(guān)鍵幀進(jìn)行控制動(dòng)畫效果。

<KeyFrameSet>
    <KeyPosition
        app:framePosition="50"
        app:keyPositionType="parentRelative"
        app:motionTarget="@+id/v_swipe"
        app:percentY="0.3" />
    <KeyAttribute
        android:alpha="0"
        app:framePosition="50"
        app:motionTarget="@+id/v_swipe" />
</KeyFrameSet>

keyAttribute 是用于在過渡動(dòng)畫中控制 View 的屬性,比如在動(dòng)畫執(zhí)行 50% 時(shí),View 的 alpha 值為 0 ,那么在從 0 - 50% 及 50% - 100% 的過程中,則由 MotionLayout 根據(jù)其執(zhí)行時(shí)間自動(dòng)改變 View 的狀態(tài)。

image

剛才聊的都是關(guān)于動(dòng)畫本身的內(nèi)容,實(shí)際上,MotionLayout 提供更多方式來對(duì) View 進(jìn)行狀態(tài)改變,不只是通過在 ConstraintSet 中指定 Layout 來改變 View 的相對(duì)位置,它還提供了更為豐富的方法進(jìn)行改變 View 的狀態(tài),比如:

Motion 用于改變動(dòng)畫效果,例如加速、減速、先水平方法還是先垂直方向進(jìn)行移動(dòng)

CustomAttribute 用于改變自定義屬性;

PropertySet 用于改變 View 特定的幾個(gè)屬性;

Transform 用于改變 View 中涉及到屬性動(dòng)畫的屬性,例如:rotation、scaleX 等。用法也很簡(jiǎn)單,像 Layout 那樣聲明出來即可。

<ConstraintSet android:id="@+id/v_swipe_start">
    <Constraint android:id="@+id/v_swipe">
        <CustomAttribute
            app:attributeName="backgroundColor"
            app:customColorValue="@color/colorAccent" />
        <Transform
            android:scaleX="1.0"
            android:scaleY="1.0" />
        <Layout
            android:layout_width="48dp"
            android:layout_height="48dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </Constraint>

</ConstraintSet>

<ConstraintSet android:id="@+id/v_swipe_end">
    <Constraint android:id="@+id/v_swipe">
        <CustomAttribute
            app:attributeName="backgroundColor"
            app:customColorValue="@color/colorPrimary" />
        <Transform
            android:scaleX="3.0"
            android:scaleY="3.0" />
        <Layout
            android:layout_width="48dp"
            android:layout_height="48dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </Constraint>

</ConstraintSet>
image

由于放大倍數(shù)較大,超出屏幕,所以在結(jié)束狀態(tài)時(shí)顯示存在異常。

怎么樣,MotionLayout 是不是比想象中的好玩一些,就是現(xiàn)在不太方便調(diào)試,每次調(diào)試都需要運(yùn)行,不過呢,現(xiàn)在這個(gè)還沒發(fā)布正式版,估計(jì)在正式版中 Google 應(yīng)該會(huì)解決這個(gè)問題。

本文首發(fā)于個(gè)人博客,文中全部源代碼已上傳至 GitHub,代碼分支為:motionLayout。喜歡的麻煩點(diǎn)個(gè)??。

推薦學(xué)習(xí)網(wǎng)站:CodeLab

本文封面圖:Photo by NASA on Unsplash

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容