ConstraintLayout2.0新特性詳解及實戰

NO IMAGE

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發佈

ConstraintHelper

ConstraintLayout在 1.0 的時候提供了 GuideLine 輔助佈局,在 1.1 時提供了 Group 和 Barrier,在 2.0 時候提供了Layer以及放開了限制,開發者可以自定義 Helper 了。

Group (Added in 1.1)

Group可以用來控制一組view的可見性

    <android.support.constraint.Group
android:id="@+id/group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="button4,button9" />

可以通過控制 group 的 hide/show 來直接控制一組 view(button4,button9) 的可見性。

Barrier (Added in 1.1)

來看一個場景,下面是一個表單,Email 和 Password 左對齊 中間的虛線為 GuideLine,具體字段都和GuideLine左對齊。

ConstraintLayout2.0新特性詳解及實戰

現在如果需要做多語言,翻譯為德文後變成了下面的效果

ConstraintLayout2.0新特性詳解及實戰

這時候就需要Barrier出場了,Barrier是柵欄的意思,可以理解為擋著不讓超過。

ConstraintLayout2.0新特性詳解及實戰

改進方法

  • 把中間的虛線GuideLine換成Barrier
  • 把①和②加入到Barrier的referenced_ids中
  • 指定barrierDirection為right(右側不超過)
  • 把③和④左邊對齊到Barrier的右邊

這樣 Email 和 Password就不會超出Barrier,大致代碼如下(有刪減,完整代碼參考這裡)

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout>
<TextView
android:id="@+id/tv_email"
app:layout_constraintBottom_toTopOf="@+id/tv_password"
app:layout_constraintStart_toStartOf="@+id/tv_password"
app:layout_constraintTop_toTopOf="parent"
tools:text="E-mail Addresse" />
<EditText
android:id="@+id/et_email"
android:text="[email protected]"
app:layout_constraintBaseline_toBaselineOf="@+id/tv_email"
app:layout_constraintStart_toEndOf="@+id/barrier" />
<TextView
android:id="@+id/tv_password"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_email" />
<EditText
android:id="@+id/et_password"
android:inputType="textPassword"
android:text="2321321"
app:layout_constraintBaseline_toBaselineOf="@+id/tv_password"
app:layout_constraintStart_toEndOf="@+id/barrier" />
<android.support.constraint.Barrier
android:id="@+id/barrier"
app:barrierDirection="right"
app:constraint_referenced_ids="tv_email,tv_password" />
</android.support.constraint.ConstraintLayout>

Layer (Added in 2.0)

Layer 可以看作是它引用的 view 的邊界(可以理解為包含這些 view 的一個 ViewGroup,但是Layer並不是ViewGroup,Layer並不會增加 view 的層級)。另外Layer支持對裡面的 view 一起做變換。

考慮這麼一個場景,如果一個頁面裡面有部分 view 需要加個背景,使用Layer引用這幾個 view,然後給Layer設置背景就可以了。如果不用Layer,只能另外加個 ViewGroup 包住這幾個 View 了,這樣會增加 view 的層級,不利於性能。

看一個示例(完整代碼):

ConstraintLayout2.0新特性詳解及實戰

圖中Layer包住了中間的 6 個按鈕,綠色邊線白色填充是通過 Layer設置背景完成的。另外對Layer裡面的所有按鈕一起做動畫,出來的效果就是這樣子

ConstraintLayout2.0 除了提供幾個默認實現的ConstraintHelper外,還提供開發者自定義ConstraintHelper的方式。

自定義 Helper

為什麼需要自定義?

  • 保持 view 的層級不變,不像 ViewGroup 會增加 view 的層級
  • 封裝一些特定的行為,方便複用
  • 一個 View 可以被多個 Helper引用,可以很方便組合出一些複雜的效果出來

如何自定義?

  • Helper持有 view 的引用,所以可以獲取 view (getViews)然後操作 view
  • 提供了 onLayout 前後的 callback(updatePreLayout/updatePreLayout)
  • Helper 繼承了 view,所以Helper本身也是 view

CircularRevealHelper

ConstraintLayout2.0新特性詳解及實戰

對一張圖片作出CircularReveal的效果
ViewAnimationUtils給我們提供了createCircularReveal這個函數

public static Animator createCircularReveal(View view,
int centerX,  int centerY, float startRadius, float endRadius) 

藉助這個函數只需要計算出中心點(centerX,centerY)和 endRadius(半徑)就可以很方便實現CircularReveal的效果

class CircularRevealHelper @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {
override fun updatePostLayout(container: ConstraintLayout) {
super.updatePostLayout(container)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val views = getViews(container)
for (view in views) {
val anim = ViewAnimationUtils.createCircularReveal(view, view.width / 2,
view.height / 2, 0f,
Math.hypot((view.height / 2).toDouble(), (view.width / 2).toDouble()).toFloat())
anim.duration = 3000
anim.start()
}
}
}
}

updatePostLayout會在 onLayout 之後調用,在這裡做動畫就可以。

有了CircularRevealHelper之後可以直接在 xml 裡面使用,在CircularRevealHelper的constraint_referenced_ids裡面指定需要做動畫 view。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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">
<ImageView
android:id="@+id/img_mario"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mario" />
<cn.feng.constraintLayout2.helps.CircularRevealHelper
android:id="@+id/helper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="img_mario"
tools:ignore="MissingConstraints" />
</android.support.constraint.ConstraintLayout>

後面如果要對 view 做CircularReveal直接在 xml 裡面指定就可以了,做到了很好的複用。

FlyinHelper

ConstraintLayout2.0新特性詳解及實戰

再來看看這個 Flyin 的飛入效果,view 從四周飛入到各自的位置。

這個動畫的關鍵在於計算出每個 view 該從什麼方向飛入。

ConstraintLayout2.0新特性詳解及實戰

紅色邊框的位置可以藉助前面介紹的的Layer找到(當然也可以不借助Layer,自己算,稍顯複雜),從而計算出紅色框框部分的中間點位置,
再和圖中每個 view 的中間點比較(圖中每個白點的位置)從而得出每個 view 該從哪個方向飛入。

計算每個view 的初始位置代碼如下,藉助上面的圖形應該很好理解。

    for (view in views) {
val viewCenterX = (view.left + view.right) / 2
val viewCenterY = (view.top + view.bottom) / 2
val startTranslationX = if (viewCenterX < centerPoint.x) -2000f else 2000f
val startTranslationY = if (viewCenterY < centerPoint.y) -2000f else 2000f
view.translationX = (1 - animatedFraction) * startTranslationX
view.translationY = (1 - animatedFraction) * startTranslationY
}

FlyinHelper 的完整代碼參考這裡

ComposeMultipleHelper

每個 view 不但可以接受一個ConstraintHelper,還可以同時接受多個ConstraintHelper。

ConstraintLayout2.0新特性詳解及實戰

左邊的四個 ImageView 和右下的 FloatingActionButton 都有 Flyin 的效果,同時左邊的四個ImageView還在繞 Y 軸做 3D 旋轉。上方的 Seekbar的背景在做CircularReveal的效果。有了前面編寫的CircularRevealHelper以及 FlyInHelper 我們可以很方便做到這樣的效果。

代碼參考這裡

Flow (VirtualLayout)

Flow 是 VirtualLayout,Flow 可以像 Chain 那樣幫助快速橫向/縱向佈局constraint_referenced_ids裡面的元素。
通過flow_wrapMode可以指定具體的排列方式,有三種模式

  • wrap none : 簡單地把constraint_referenced_ids裡面的元素組成chain,即使空間不夠

ConstraintLayout2.0新特性詳解及實戰

  • wrap chain : 根據空間的大小和元素的大小組成一條或者多條 chain

ConstraintLayout2.0新特性詳解及實戰

  • wrap aligned : wrap chain類似,但是會對齊

ConstraintLayout2.0新特性詳解及實戰

下面看下如何實現這個計算器佈局:

ConstraintLayout2.0新特性詳解及實戰

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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=".activity.MainActivity">
<android.support.constraint.helper.Flow
android:id="@+id/flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFC107"
android:padding="20dp"
app:constraint_referenced_ids="tv_num_7,tv_num_8,tv_num_9,tv_num_4,tv_num_5,tv_num_6,tv_num_1,tv_num_2,tv_num_3,tv_num_0,tv_operator_div,tv_dot,tv_operator_times"
app:flow_horizontalGap="10dp"
app:flow_maxElementsWrap="3"
app:flow_verticalGap="10dp"
app:flow_wrapMode="aligned"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_num_7"
style="@style/text_view_style"
android:text="7" />
<TextView
android:id="@+id/tv_num_8"
style="@style/text_view_style"
android:text="8" />
<TextView
android:id="@+id/tv_num_9"
style="@style/text_view_style"
android:text="9" />
<TextView
android:id="@+id/tv_num_4"
style="@style/text_view_style"
android:text="4" />
<TextView
android:id="@+id/tv_num_5"
style="@style/text_view_style"
android:text="5" />
<TextView
android:id="@+id/tv_num_6"
style="@style/text_view_style"
android:text="6" />
<TextView
android:id="@+id/tv_num_1"
style="@style/text_view_style"
android:text="1" />
<TextView
android:id="@+id/tv_num_2"
style="@style/text_view_style"
android:text="2" />
<TextView
android:id="@+id/tv_num_3"
style="@style/text_view_style"
android:text="3" />
<TextView
android:id="@+id/tv_num_0"
style="@style/text_view_style"
android:text="0" />
<TextView
android:id="@+id/tv_operator_div"
style="@style/text_view_style"
android:text="/"
tools:layout_editor_absoluteX="156dp"
tools:layout_editor_absoluteY="501dp" />
<TextView
android:id="@+id/tv_operator_times"
style="@style/text_view_style"
android:text="*" />
<TextView
android:id="@+id/tv_dot"
style="@style/text_view_style"
android:text="."
tools:layout_editor_absoluteX="278dp"
tools:layout_editor_absoluteY="501dp" />
<TextView
android:id="@+id/KE"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#00BCD4"
android:gravity="center"
android:text="Compute"
android:textColor="@android:color/white"
android:textSize="24sp"
app:layout_constraintBottom_toBottomOf="@+id/tv_operator_times"
app:layout_constraintEnd_toEndOf="@+id/tv_dot"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/tv_operator_div"
app:layout_constraintTop_toTopOf="@+id/tv_operator_times" />
<TextView
android:id="@+id/KR"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#03A9F4"
android:gravity="right|center_vertical"
android:paddingEnd="16dp"
android:text="0"
android:textColor="@android:color/white"
android:textSize="58sp"
app:layout_constraintBottom_toTopOf="@+id/flow"
app:layout_constraintEnd_toEndOf="@+id/flow"
app:layout_constraintStart_toStartOf="@+id/flow"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

藉助 flow 很快可以佈局出來,這裡flow_wrapMode使用的是aligned,id 為KE的TextView可以對齊到 Flow 裡面的 view,id 為KR的TextView可以對齊到 Flow,另外 Flow 也是ConstraintHelper,所以Flow 也是個 View,可以設置背景,padding等元素。
那麼這樣佈局有什麼優勢?
這樣的佈局 view 都在一個層級,不使用 ViewGroup,減少層級。

流式 APIs

1.1 之前需要這樣修改屬性

    val set = ConstraintSet()
set.clone(constraintLayout)
set.setTranslationZ(R.id.image, 32f)
set.setMargin(R.id.image, ConstraintSet.START, 43)
set.applyTo(constraintLayout)

2.0 提供了ConstraintProperties
可以使用流式 API 修改屬性

 val properties = ConstraintProperties(findViewById(R.id.image))
properties.translationZ(32f)
.margin(ConstraintSet.START, 43)
.apply()

MotionLayout

關於 MotionLayout 可以參考ConstraintLayout開發者 Nicolas Roard
寫的系列文章,

Introduction to MotionLayout (part I)

Introduction to MotionLayout (part II)

Introduction to MotionLayout (part III)

Defining motion paths in MotionLayout


完整代碼參考 Github,喜歡的話 star 哦


參考資料

ConstraintLayout Deep Dive (Android Dev Summit ’18)

ConstraintLayout 2.0 by Nicolas Roard and John Hoford, Google EN

What’s New in ConstraintLayout (Google I/O’19)

相關文章

深入源碼學習Androiddatabinding之:回調通知管理器CallbackRegistry解析

深入源碼學習androiddatabinding之:ViewDataBinding

深入源碼學習androiddatabinding之:databinding註解

深入源碼學習androiddatabinding之:源碼的正確打開姿勢