MotionLayout - 折疊工具欄

在Google IO 2018上宣布了ConstraintLayout 2.0碰酝,最大的新增功能是MotionLayout,它為我們提供了一個(gè)用于布局動(dòng)畫(huà)的驚人新工具使兔。Nicolas Roard已經(jīng)發(fā)表了對(duì)MotionLayout精彩介紹默色,我強(qiáng)烈建議您閱讀一下,以了解MotionLayout的基本知識(shí)和組件糕篇。在這個(gè)簡(jiǎn)短的系列中啄育,我們將看看如何使用MotionLayout創(chuàng)建一個(gè)我們都應(yīng)該熟悉的行為:折疊工具欄。

在開(kāi)始之前拌消,值得一提的是在CoordinatorLayout中使用CollapsingToolbarLayout來(lái)實(shí)現(xiàn)此行為并沒(méi)有任何錯(cuò)誤挑豌。此外,如果您已經(jīng)在應(yīng)用程序中工作墩崩,那么通過(guò)更改幾乎無(wú)法獲得氓英。也就是說(shuō),哪個(gè)CoordinatorLayout提供了一些非常有用的行為鹦筹,試圖調(diào)整它們甚至創(chuàng)建自己的自定義行為是相當(dāng)困難的铝阐。正是在這種情況下,MotionLayout可以提供更大的靈活性铐拐,而且我的早期實(shí)驗(yàn)表明徘键,實(shí)現(xiàn)您想要的更容易。此外遍蟋,MotionLayout開(kāi)辟了一些非常難以實(shí)現(xiàn)的新行為CoordinatorLayout吹害。

MotionLayout與Android上許多其他動(dòng)畫(huà)框架之間的主要區(qū)別之一是視圖動(dòng)畫(huà)和屬性動(dòng)畫(huà)在給定的持續(xù)時(shí)間內(nèi)運(yùn)行。雖然可以指定持續(xù)時(shí)間并取消正在運(yùn)行的動(dòng)畫(huà)虚青,但是無(wú)法根據(jù)用戶(hù)輸入控制正在運(yùn)行的動(dòng)畫(huà)它呀。例如,折疊工具欄應(yīng)根據(jù)用戶(hù)滾動(dòng)進(jìn)行展開(kāi)和折疊,實(shí)際動(dòng)畫(huà)應(yīng)遵循用戶(hù)的拖動(dòng)纵穿。這些框架根本不可能實(shí)現(xiàn)這一點(diǎn)下隧。

讓我們首先看看我們?cè)噲D模仿的行為。這是一個(gè)折疊工具欄谓媒,它使用材料組件庫(kù)和CoordinatorLayout中的CollapsingToolbarLayout實(shí)現(xiàn):

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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">
 
    <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/recyclerview"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintTop_toBottomOf="@id/appbar"
      app:layout_behavior="@string/appbar_scrolling_view_behavior" />
 
    <com.google.android.material.appbar.AppBarLayout
      android:id="@+id/appbar"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:fitsSystemWindows="true"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
 
      <com.google.android.material.appbar.CollapsingToolbarLayout
        android:id="@+id/collapsing_toolbar"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        android:fitsSystemWindows="true"
        app:contentScrim="?attr/colorPrimary"
        app:expandedTitleGravity="bottom"
        app:expandedTitleMarginEnd="@dimen/activity_horizontal_margin"
        app:expandedTitleMarginStart="@dimen/activity_horizontal_margin"
        app:layout_scrollFlags="scroll|exitUntilCollapsed"
        app:title="@string/app_name">
 
        <ImageView
          android:id="@+id/toolbar_image"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:adjustViewBounds="true"
          android:contentDescription="@null"
          android:fitsSystemWindows="true"
          android:scaleType="centerCrop"
          android:src="@drawable/beach_huts" />
 
        <androidx.appcompat.widget.Toolbar
          android:id="@+id/toolbar"
          android:layout_width="match_parent"
          android:layout_height="?attr/actionBarSize"
          app:layout_collapseMode="pin"
          app:popupTheme="@style/ThemeOverlay.AppCompat" />
      </com.google.android.material.appbar.CollapsingToolbarLayout>
    </com.google.android.material.appbar.AppBarLayout>
  
</androidx.coordinatorlayout.widget.CoordinatorLayout>

我們從中獲得的行為是這樣的:


traditional.gif

使用MotionLayout獲得近似值非常簡(jiǎn)單淆院。我們首先從布局文件開(kāi)始:

<?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"
  tools:context=".MainActivity"
  app:layoutDescription="@xml/collapsing_toolbar"
  tools:showPaths="true">
 
  <androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerview"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/toolbar_image" />
 
  <ImageView
    android:id="@+id/toolbar_image"
    android:layout_width="0dp"
    android:layout_height="200dp"
    android:adjustViewBounds="true"
    android:contentDescription="@null"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:fitsSystemWindows="true"
    android:scaleType="center"
    android:src="@drawable/beach_huts"
    android:background="@color/colorPrimary" />
 
  <ImageView
    android:id="@android:id/home"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:paddingTop="16dp"
    android:paddingBottom="16dp"
    android:src="@drawable/abc_ic_ab_back_material"
    android:tint="?android:attr/textColorPrimaryInverse"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
 
  <TextView
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginBottom="24dp"
    android:text="@string/app_name"
    android:textColor="?android:attr/textColorPrimaryInverse"
    android:textSize="32sp"
    android:textStyle="bold"
    app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
    app:layout_constraintStart_toStartOf="parent" />
 
</androidx.constraintlayout.motion.widget.MotionLayout>

這本質(zhì)上是我們可以使用ConstraintLayout創(chuàng)建的標(biāo)準(zhǔn)布局,唯一的區(qū)別是父實(shí)際上是MotionLayout(它擴(kuò)展了ConstraintLayout篙耗,所以我們可以像普通的ConstraintLayout一樣使用MotionLayout)迫筑。該MotionLayout有一個(gè)名為屬性這哪里是奇跡發(fā)生。我故意在這里使用基本的View類(lèi)型來(lái)清楚地表明沒(méi)有來(lái)自Views本身的行為宗弯。在真正的應(yīng)用程序中脯燃,我將使用AppBarLayout和工具欄。app:layoutDescription

如果我們?cè)谠O(shè)計(jì)工具中查看它蒙保,我們可以看到這表示工具欄處于展開(kāi)狀態(tài)時(shí)的布局:

activity_main.png

我剛才提到魔法發(fā)生在app:layoutDescription屬性中引用的文件中辕棚,所以讓我們來(lái)看看:

<?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/collapsed"
    app:constraintSetStart="@id/expanded">
 
    <OnSwipe
      app:dragDirection="dragUp"
      app:touchAnchorId="@id/recyclerview"
      app:touchAnchorSide="top" />
 
  </Transition>
 
  <ConstraintSet android:id="@+id/expanded">
    <Constraint
      android:id="@id/toolbar_image"
      android:layout_height="200dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">
      <CustomAttribute
        app:attributeName="imageAlpha"
        app:customIntegerValue="255" />
    </Constraint>
    <Constraint
      android:id="@id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:layout_marginBottom="24dp"
      android:scaleX="1.0"
      android:scaleY="1.0"
      app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
      app:layout_constraintStart_toStartOf="parent">
    </Constraint>
  </ConstraintSet>
 
  <ConstraintSet android:id="@+id/collapsed">
    <Constraint
      android:id="@id/toolbar_image"
      android:layout_height="?attr/actionBarSize"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">
      <CustomAttribute
        app:attributeName="imageAlpha"
        app:customIntegerValue="0" />
    </Constraint>
    <Constraint
      android:id="@id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="20dp"
      android:layout_marginBottom="0dp"
      android:scaleX="0.625"
      android:scaleY="0.625"
      app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="@id/toolbar_image">
    </Constraint>
 
  </ConstraintSet>
 
</MotionScene>

所以這對(duì)MotionLayout來(lái)說(shuō)是全新的,可能看起來(lái)有些可怕邓厕,所以讓我們把它分解成更小逝嚎,更易于管理的塊。父布局是一個(gè)MotionScene详恼,它包含定義轉(zhuǎn)換的所有組件补君。它包含兩個(gè)ConstraintSet,每個(gè)ConstraintSet定義一組約束昧互,表示布局的固定狀態(tài)挽铁。我們稍后將詳細(xì)介紹這些內(nèi)容,但是現(xiàn)在只需要了解一個(gè)ConstraintSet表示工具欄處于完全展開(kāi)狀態(tài)敞掘,另一個(gè)表示工具欄處于完全折疊狀態(tài)叽掘。

所述過(guò)渡元素定義什么這些開(kāi)始和結(jié)束狀態(tài)是,如何在兩者之間的轉(zhuǎn)換由用戶(hù)交互來(lái)控制:

<Transition
    app:constraintSetEnd="@id/collapsed"
    app:constraintSetStart="@id/expanded">
 
    <OnSwipe
      app:dragDirection="dragUp"
      app:touchAnchorId="@id/recyclerview"
      app:touchAnchorSide="top" />
 
  </Transition>

在app:constraintSetStart和app:constraintSetEnd屬性是兩個(gè)引用ConstrainSet定義的展開(kāi)和折疊狀態(tài)s玖雁。該OnSwipe元素結(jié)合在轉(zhuǎn)變到用戶(hù)的拖動(dòng)RecyclerView在我們前面的主要布局文件更扁。在展開(kāi)和折疊狀態(tài)下,RecyclerView的頂部邊緣位于不同的位置赫冬,因?yàn)樗幌拗圃趲в蠭D 的ImageView的底部邊緣toolbar_image浓镜,并且這種轉(zhuǎn)換完全是關(guān)于控制該變量位置,并且該控制來(lái)自用戶(hù)在RecyclerView上拖動(dòng)劲厌。在這10行XML中竖哩,我們正在完成大量的工作。內(nèi)部結(jié)構(gòu)非常復(fù)雜脊僚,因?yàn)樗或?qū)逐出RecyclerView的滾動(dòng)行為。

要理解兩個(gè)ConstrainSet定義,讓我們首先考慮我們需要控制的兩件事辽幌。第一個(gè)是ImageView增淹,它表示背景(帶ID toolbar_image)更改高度,圖像不透明度發(fā)生變化乌企。通過(guò)更改高度虑润,它還將導(dǎo)致RecyclerView的頂部移動(dòng),因?yàn)楹笳弑患s束到此ImageView的底部加酵。第二個(gè)視圖是TextView拳喻,它包含title需要移動(dòng)和更改大小的標(biāo)題(帶ID )。

讓我們首先看一下ImageView的兩個(gè)狀態(tài)之間的差異猪腕。在擴(kuò)展?fàn)顟B(tài)下冗澈,它是這樣的:

  <ConstraintSet android:id="@+id/expanded">
    <Constraint
      android:id="@id/toolbar_image"
      android:layout_height="200dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">
      <CustomAttribute
        app:attributeName="imageAlpha"
        app:customIntegerValue="255" />
    </Constraint>

因此

 <ConstraintSet android:id="@+id/collapsed">
    <Constraint
      android:id="@id/toolbar_image"
      android:layout_height="?attr/actionBarSize"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent">
      <CustomAttribute
        app:attributeName="imageAlpha"
        app:customIntegerValue="0" />
    </Constraint>

這里只有兩個(gè)小的差異。首先是layout_height陋葡,第二個(gè)是CustomAttribute命名imageAlpha亚亲。CustomAttribute這個(gè)名稱(chēng)可能意味著我們正在使用自定義View,但事實(shí)并非如此腐缤。雖然我們使用一個(gè)標(biāo)準(zhǔn)的ImageView捌归,在主屬性約束的元素約束集可以是任何的屬性ConstraintLayout.LayoutParams或任何屬性的查看,但查看的子類(lèi)岭粤,如ImageView的惜索,我們需要使用一個(gè)CustomAttribute實(shí)際上與ObjectAnimator非常相似。在這種情況下剃浇,我們正在調(diào)整imageAlphaImageView的屬性巾兆。當(dāng)然,您也可以將此技術(shù)用于自定義視圖的自定義屬性偿渡,就像ObjectAnimator一樣臼寄。

TextView實(shí)際上非常相似。擴(kuò)展?fàn)顟B(tài)是:

    <Constraint
      android:id="@id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="8dp"
      android:layout_marginBottom="24dp"
      android:scaleX="1.0"
      android:scaleY="1.0"
      app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
      app:layout_constraintStart_toStartOf="parent" />

崩潰的狀態(tài)是:

    <Constraint
      android:id="@id/title"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="20dp"
      android:layout_marginBottom="0dp"
      android:scaleX="0.625"
      android:scaleY="0.625"
      app:layout_constraintBottom_toBottomOf="@id/toolbar_image"
      app:layout_constraintStart_toStartOf="parent" 
      app:layout_constraintTop_toTopOf="@id/toolbar_image"/>

這里我們使用視圖縮放來(lái)改變TextView的大小溜宽。如果您想知道為什么我選擇了視圖縮放而不是textSize通過(guò)CustomAttribute進(jìn)行更改吉拳,原因是更改文本大小并重新渲染它在計(jì)算上比僅僅應(yīng)用轉(zhuǎn)換要昂貴得多,因此我們不太可能使用這種技術(shù)獲得過(guò)渡适揉。

我們正在做的另一件事是改變邊距留攒,以及TextView相對(duì)于ImageView的定位方式。在折疊狀態(tài)下嫉嘀,它垂直居中炼邀,在展開(kāi)狀態(tài)下,它與底部對(duì)齊剪侮,因此TextView將更加相對(duì)于ImageView的大小拭宁。

如果我們使用該布局代替我們開(kāi)始的CoordinatorLayout實(shí)現(xiàn)洛退,我們會(huì)得到以下行為:

motion_basic.gif

這實(shí)際上非常接近,但是鷹眼可能會(huì)發(fā)現(xiàn)它與我們?cè)陂_(kāi)始時(shí)看到的CoordinatorLayout方法之間存在細(xì)微差別:在CoordinatorLayout轉(zhuǎn)換中杰标,圖像淡入不會(huì)在轉(zhuǎn)換過(guò)程中發(fā)生兵怯,因?yàn)樗cMotionLayout版本。在本系列的結(jié)束文章中腔剂,我們將介紹一些使用MotionLayout可以獲得的更細(xì)粒度的控件媒区。

這里提供了本文的源代碼。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掸犬,一起剝皮案震驚了整個(gè)濱河市袜漩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌湾碎,老刑警劉巖貌矿,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雪侥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)件缸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)期犬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)茧痕,“玉大人句葵,你說(shuō)我怎么就攤上這事±缺椋” “怎么了嬉愧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)喉前。 經(jīng)常有香客問(wèn)我没酣,道長(zhǎng),這世上最難降的妖魔是什么卵迂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任裕便,我火速辦了婚禮,結(jié)果婚禮上见咒,老公的妹妹穿的比我還像新娘偿衰。我一直安慰自己,他們只是感情好改览,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布下翎。 她就那樣靜靜地躺著,像睡著了一般宝当。 火紅的嫁衣襯著肌膚如雪视事。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,784評(píng)論 1 290
  • 那天庆揩,我揣著相機(jī)與錄音俐东,去河邊找鬼跌穗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虏辫,可吹牛的內(nèi)容都是我干的瞻离。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼乒裆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了推励?” 一聲冷哼從身側(cè)響起鹤耍,我...
    開(kāi)封第一講書(shū)人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎验辞,沒(méi)想到半個(gè)月后稿黄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡跌造,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年杆怕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壳贪。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡陵珍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出违施,到底是詐尸還是另有隱情互纯,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布磕蒲,位于F島的核電站留潦,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏辣往。R本人自食惡果不足惜兔院,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望站削。 院中可真熱鬧坊萝,春花似錦、人聲如沸钻哩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)街氢。三九已至扯键,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間珊肃,已是汗流浹背荣刑。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工馅笙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厉亏。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓董习,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親爱只。 傳聞我的和親對(duì)象是個(gè)殘疾皇子皿淋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容