一文快速入門 ConstraintLayout

ConstraintLayout 目前是 Android Studio 的默認佈局,其優勢就是可以使用扁平化的視圖層次結構(無嵌套視圖組)來創建複雜多變的大型佈局,在繪製效率上相對其它佈局有很大優勢。ConstraintLayout 與 RelativeLayout 相似,其中所有的視圖均根據同級視圖與父佈局之間的關係來進行定位,但其靈活性要高於 RelativeLayout,並且更易於與 Android Studio 的佈局編輯器配合使用

ConstraintLayout 能夠靈活地定位和調整子 View 的大小,子 View 依靠約束關係來確定位置,且每個子 View 必須至少有一個水平約束條件加一個垂直約束條件,每個約束條件均表示與其它視圖、父佈局或隱形引導線之間連接或對齊方式。在一個約束關係中,需要有一個 Source(源)以及一個 Target(目標),Source 的位置依賴於 Target,可以理解爲通過約束關係 Source 與 Target 鏈接在了一起,Source 相對於 Target 的位置便是固定的了

引入當前最新的 release 版本:

dependencies {
    implementation "androidx.constraintlayout:constraintlayout:2.0.4"
}

一、相對定位

ConstraintLayout 最基本的屬性包含以下幾個,即 layout_constraintXXX_toYYYOf 格式的屬性,用於將 ViewA 的 XXX 方向和 ViewB 的 YYY 方向進行約束。當中,ViewB 也可以是父容器 ConstraintLayout,用 parent 來表示。這些屬性都是用於爲控件添加垂直和水平方向的約束力,根據約束力的 “有無” 或者 “強弱”,控件會處於不同的位置

  • layout_constraintLeft_toLeftOf
  • layout_constraintLeft_toRightOf
  • layout_constraintRight_toLeftOf
  • layout_constraintRight_toRightOf
  • layout_constraintTop_toTopOf
  • layout_constraintTop_toBottomOf
  • layout_constraintBottom_toTopOf
  • layout_constraintBottom_toBottomOf
  • layout_constraintStart_toEndOf
  • layout_constraintStart_toStartOf
  • layout_constraintEnd_toStartOf
  • layout_constraintEnd_toEndOf
  • layout_constraintBaseline_toBaselineOf

例如,根據約束的不同,控件在不同的方向上進行對齊

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <TextView
        android:id="@+id/tv1"
        android:layout_width="200dp"
        android:layout_height="150dp"
        android:layout_margin="20dp"
        android:background="#f5ec7e"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/tv2"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="20dp"
        android:background="#68b0f9"
        android:gravity="center"
        android:text="沒有設置底部約束,所以只會頂部和黃色方塊對齊"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/tv1"
        app:layout_constraintTop_toTopOf="@+id/tv1" />

    <TextView
        android:id="@+id/tv3"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="20dp"
        android:background="#984ff7"
        android:gravity="center"
        android:text="上下均設置了約束,所以會居於中間"
        app:layout_constraintBottom_toBottomOf="@+id/tv1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/tv1"
        app:layout_constraintTop_toBottomOf="@+id/tv2" />

    <TextView
        android:id="@+id/tv4"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#9C27B0"
        android:gravity="center"
        android:text="屏幕各個方向居中"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

二、約束力的強度

如果想要讓控件的左右或者上下間距具有固定的比例,這種即在某個方向上其兩邊的約束力的強度有所不同,可以依靠 layout_constraintHorizontal_biaslayout_constraintVertical_bias 兩個屬性來設置控件在水平和垂直方向的偏移量

例如,可以來控制 TextView 的左右或者上下間距的百分比

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <TextView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#FF9800"
        android:gravity="center"
        android:text="公衆號:字節數組"
        android:textColor="@android:color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.1" />

    <TextView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#2196F3"
        android:gravity="center"
        android:text="https://github.com/leavesC"
        android:textColor="@android:color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.9" />

    <TextView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#4CAF50"
        android:gravity="center"
        android:text="Hello World!"
        android:textColor="@android:color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

三、寬高比

在使用其它佈局類型時,如果想讓控件在不同的屏幕上都保持固定的寬高比是比較麻煩的,但用 ConstraintLayout 就很簡單。例如,如果我們想爲 Activity 實現一個固定寬高比的頂部標題欄的話,可以將寬度設置爲佔滿屏幕,高設置爲 0dp,然後通過 app:layout_constraintDimensionRatio 屬性設定寬高比爲一個固定比例,此時 ConstraintLayout 就會自動根據屏幕的寬度來動態計算標題欄應該具有的高度

此外,要使用layout_constraintDimensionRatio屬性,需要其寬度或者高度當中有一個值是可知的,且剩下的一個是 0dp。所謂的可知,即該值是已經具備了明確的約束條件。控件的寬高尺寸比例則通過 “float值” 或者 “寬度 : 高度” 的形式來設置,通過在比例值的前面添加 w 或者 h 來指明比例值是根據寬度還是高度來進行計算

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <TextView
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#f5ec7e"
        android:gravity="center"
        android:text="公衆號:字節數組"
        android:textColor="@android:color/black"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintDimensionRatio="h,15:2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:layout_width="100dp"
        android:layout_height="0dp"
        android:background="#5476fd"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="W,1:1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

四、控件之間的寬高佔比

ConstraintLayout 也可以像 LinearLayout 一樣爲子控件設置 layout_weight 屬性,從而控件子控件之間的寬高佔比,對應的屬性是:layout_constraintHorizontal_weightlayout_constraintVertical_weight

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <TextView
        android:id="@+id/tv1"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#f5ec7e"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintBottom_toTopOf="@id/tv4"
        app:layout_constraintEnd_toStartOf="@+id/tv2"
        app:layout_constraintHorizontal_weight="3"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_weight="1" />

    <TextView
        android:id="@+id/tv2"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:background="#55e4f4"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintEnd_toStartOf="@+id/tv3"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintStart_toEndOf="@+id/tv1"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv3"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:background="#f186ad"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@+id/tv2"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv4"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#03A9F4"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintBottom_toTopOf="@id/tv5"
        app:layout_constraintEnd_toEndOf="@id/tv1"
        app:layout_constraintStart_toStartOf="@id/tv1"
        app:layout_constraintTop_toBottomOf="@id/tv1"
        app:layout_constraintVertical_weight="1" />

    <TextView
        android:id="@+id/tv5"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#F44336"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@id/tv4"
        app:layout_constraintStart_toStartOf="@id/tv4"
        app:layout_constraintTop_toBottomOf="@id/tv4"
        app:layout_constraintVertical_weight="1" />

</androidx.constraintlayout.widget.ConstraintLayout>

五、Dimensions

當控件的寬或者高設置爲 0dp 時,可以用以下兩個屬性來指定控件的寬度或高度佔父控件空間的百分比,屬性值在 0 到 1 之間

  1. layout_constrainWidth_percent
  2. layout_constrainHeight_percent
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <Button
        android:id="@+id/btn_target"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Target Button"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_percent="0.8" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Source Button"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_target"
        app:layout_constraintWidth_percent="0.5" />

</androidx.constraintlayout.widget.ConstraintLayout>

六、Visibility

在使用其它佈局時,如果將 View 的 visibility 屬性設置爲 gone,那麼其它原本依賴該 View 來參照定位的屬性都會失效,而在 ConstraintLayout 佈局中會有所不同

在以下佈局中,紅色方塊位於屏幕右上角與黃色方塊左下角形成的矩形的中間位置

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <TextView
        android:id="@+id/tv1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#f5ec7e"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#fa6e61"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="@+id/tv1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/tv1"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

而如果將黃色方塊的 visibility 屬性設置爲 gone,那麼紅色方塊的位置會發生變化。可以理解爲黃色方塊縮小爲一個不可見的小點,位於其原先位置的中間,而紅色方塊則改爲依照該點來進行定位

此外,紅色方塊也可以依靠以下幾個屬性來控制當黃色方塊爲 Gone 時紅色方塊的 margin 值,這類屬性只有在黃色方塊的 visibility 屬性設置爲 gone 時纔會生效

  • layout_goneMarginStart
  • layout_goneMarginEnd
  • layout_goneMarginLeft
  • layout_goneMarginTop
  • layout_goneMarginRight
  • layout_goneMarginBottom

七、圓形定位

圓形定位用於將兩個 View 以角度距離這兩個維度來進行定位,以兩個 View 的中心點作爲定位點

  1. app:layout_constraintCircle - 目標 View 的 ID
  2. app:layout_constraintCircleAngle - 對齊的角度
  3. app:layout_constraintCircleRadius  - 與目標 View 之間的距離(順時針方向,0~360度)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <ImageView
        android:id="@+id/iv_a"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon_a"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_b"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon_b"
        app:layout_constraintCircle="@id/iv_a"
        app:layout_constraintCircleAngle="45"
        app:layout_constraintCircleRadius="200dp" />

    <ImageView
        android:id="@+id/iv_c"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon_c"
        app:layout_constraintCircle="@id/iv_a"
        app:layout_constraintCircleAngle="180"
        app:layout_constraintCircleRadius="200dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

八、Guideline

當需要一個任意位置的錨點時,可以使用指示線(Guideline)來幫助定位,Guideline 是 View 的子類,使用方式和普通的 View 相同,但 Guideline 有着如下的特殊屬性:

  • 寬度和高度均爲 0
  • 可見性爲 View.GONE

即指示線只是爲了幫助其他 View 進行定位,實際上並不會出現在頁面上

例如,如下代碼加入了兩條 Guideline,可以選擇使用百分比或實際距離來設置 Guideline 的位置,並通過 orientation 屬性來設置 Guideline 的方向

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="100dp" />

    <TextView
        android:id="@+id/tv1"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:background="#f5ec7e"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintLeft_toRightOf="@+id/guideline1"
        app:layout_constraintTop_toBottomOf="@+id/guideline2" />

</androidx.constraintlayout.widget.ConstraintLayout>

設置橫向指示線距離頂部 100dp,黃色方塊根據該指示線來設定頂部位置。豎向指示線設置其橫向距離百分比爲 0.5,所以黃色方塊的左側會位於屏幕的中間位置

九、Barrier

很多時候我們都會遇到控件的寬高值隨着其包含的數據的多少而改變的情況,而此時如果有多個控件之間是相互約束的話,就比較難來設定各個控件間的約束關係了,而 Barrier(屏障)就是用於解決這種情況。Barrier 和 GuideLine 一樣是一個虛擬的 View,對界面是不可見的,只是用於輔助佈局

Barrier 可以使用的屬性有:

  1. barrierDirection:用於設置 Barrier 的位置,屬性值有:bottom、top、start、end、left、right
  2. constraint_referenced_ids:用於設置 Barrier 所引用的控件的 ID,可同時設置多個
  3. barrierAllowsGoneWidgets:默認爲 true,當 Barrier 所引用的控件爲 Gone 時,則 Barrier 的創建行爲是在已 Gone 的控件已解析的位置上進行創建。如果設置爲 false,則不會將 Gone 的控件考慮在內
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <TextView
        android:id="@+id/btn_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#03A9F4"
        android:padding="10dp"
        android:text="這是一段並不長的文本"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/btn_source"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#009688"
        android:padding="10dp"
        android:text="我也不知道說什麼好"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_target" />

    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:barrierAllowsGoneWidgets="false"
        app:barrierDirection="end"
        app:constraint_referenced_ids="btn_target,btn_source" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#E91E63"
        android:padding="10dp"
        android:text="那就隨便寫寫吧"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@id/barrier"
        app:layout_constraintTop_toBottomOf="@id/btn_target" />

</androidx.constraintlayout.widget.ConstraintLayout>

佈局文件中約束了紅色方塊必須是一直處於藍色方塊+綠色方塊這個整體的右側,此時還看不出來 Barrier 的作用,但當文本內容增多時,就可以看出來了。不管是藍色方塊還是綠色方塊的寬度變大,紅色方塊都會自動向右側移動

十、Group

Group 用於控制多個控件的可見性,先依靠 constraint_referenced_ids來綁定其它 View,之後就可以通過單獨控制 Group 的可見性從而來間接改變綁定的 View 的可見性

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <Button
        android:id="@+id/btn_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Target Button"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_source"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Source Button"
        android:textAllCaps="false"
        app:layout_constraintCircle="@id/btn_target"
        app:layout_constraintCircleAngle="45"
        app:layout_constraintCircleRadius="120dp" />

    <androidx.constraintlayout.widget.Group
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"
        app:constraint_referenced_ids="btn_target, btn_source" />

</androidx.constraintlayout.widget.ConstraintLayout>

十一、Placeholder

Placeholder (佔位符)用於和一個視圖關聯起來,通過 setContentId() 方法將佔位符轉換爲指定的視圖,即視圖將在佔位符所在位置上顯示,如果此時佈局中已包含該視圖,則視圖將從原有位置消失

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <Button
        android:id="@+id/btn_setContentId"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:onClick="setContentId"
        android:text="setContentId"
        android:textAllCaps="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_target"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon_a"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_setContentId" />

    <androidx.constraintlayout.widget.Placeholder
        android:id="@+id/placeholder"
        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" />

</androidx.constraintlayout.widget.ConstraintLayout>

        val placeholder = findViewById<Placeholder>(R.id.placeholder)
        placeholder.setContentId(R.id.iv_target)

十二、Chains

Chain 比較難描述,它是一種特殊的約束形式,多個控件通過明確的相互約束來互相約束對方的位置,從而形成一個鏈條,Chain 可以設定鏈條中的剩餘空間的分發規則

例如,以下佈局中三個 TextView 都明確規定了其左側和右側的約束條件,三個 TextView 形成了一個整體,此時它們就可以稱爲一條鏈條

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintLayoutActivity">

    <TextView
        android:id="@+id/tv1"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:background="#f5ec7e"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintEnd_toStartOf="@+id/tv2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:layout_marginTop="0dp"
        android:background="#ff538c"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintEnd_toStartOf="@+id/tv3"
        app:layout_constraintStart_toEndOf="@+id/tv1"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv3"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:background="#41c0ff"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/tv2"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

鏈條分爲水平鏈條豎直鏈條兩種,分別用 layout_constraintHorizontal_chainStylelayout_constraintVertical_chainStyle 兩個屬性來設置,屬性值有以下三種:

  • spread(默認值)
  • spread_inside
  • packed

直接看效果圖才容易理解各種效果

當值爲 spread 以及控件寬度爲 wrap_content 時

 android:layout_width="wrap_content"
 app:layout_constraintHorizontal_chainStyle="spread"

當參數值爲 spread 以及控件寬度爲 0dp 時

 android:layout_width="0dp"
 app:layout_constraintHorizontal_chainStyle="spread"

當參數值爲 spread_inside 以及控件寬度爲 wrap_content 時

 android:layout_width="wrap_content"
 app:layout_constraintHorizontal_chainStyle="spread_inside"

當參數值爲 packed 以及控件寬度爲 wrap_content 時

 android:layout_width="wrap_content"
 app:layout_constraintHorizontal_chainStyle="packed"

十三、Flow

Flow 是一種新的虛擬佈局,它專門用來構建鏈式排版效果,當出現空間不足的情況時能夠自動換行,甚至是自動延展到屏幕的另一區域。當需要對多個元素進行鏈式佈局,但不確定在運行時佈局空間的實際大小是多少時 Flow 對你來說就非常有用。你可以使用 Flow 來實現讓佈局隨着應用屏幕尺寸的變化 (比如設備發生旋轉後出現的屏幕寬度變化) 而動態地進行自適應。此外,Flow 是一種虛擬佈局,並不會作爲視圖添加到視圖層級結構中,而是僅僅引用其它視圖來輔助它們在佈局系統中完成各自的佈局功能

Flow 中最重要的一個配置選項是 wrapMode,它可以決定在內容溢出 (或出現換行) 時的佈局行爲,一共有三種模式:

  • none – 所有引用的視圖以一條鏈的方式進行佈局,如果內容溢出則溢出內容不可見
  • chain – 當出現溢出時,溢出的內容會自動換行,以新的一條鏈的方式進行佈局
  • align – 同 chain 類似,但是不以行而是以列的方式進行佈局

例如,你可以在佈局文件中引入五個 CardView,每個 CardView 的方向約束均交由 Flow 來控制,Flow 默認是以水平方向來展示,可以主動設置 android:orientation="vertical"改爲豎直方向

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".FlowActivity">

    <include
        android:id="@+id/cardView1"
        layout="@layout/item_cardview" />

    <include
        android:id="@+id/cardView2"
        layout="@layout/item_cardview" />

    <include
        android:id="@+id/cardView3"
        layout="@layout/item_cardview" />

    <include
        android:id="@+id/cardView4"
        layout="@layout/item_cardview" />

    <include
        android:id="@+id/cardView5"
        layout="@layout/item_cardview" />

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="cardView1,cardView2,cardView3,cardView4,cardView5"
        app:flow_horizontalGap="30dp"
        app:flow_verticalGap="30dp"
        app:flow_wrapMode="none"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

none

此模式下控件不會自動換行,且由於屏幕寬度無法完整展示,所以只會展示一部分內容

該模式下可以同時使用的配置項有:

  • flow_horizontalStyle = "spread|spread_inside|packed" //Chains 鏈的展示形式
  • flow_verticalStyle = "spread|spread_inside|packed"
  • flow_horizontalBias = "float" //只在 style 爲 packed 時才生效,用於控制控件在水平方向上的偏移量
  • flow_verticalBias = "float"
  • flow_horizontalGap = "dimension" //設置每個控件的左右間距
  • flow_verticalGap = "dimension"
  • flow_horizontalAlign = "start|end"
  • flow_verticalAlign = "top|bottom|center|baseline

chain

此模式下控件會自動換行,且不足一行的內容會居中顯示

此模式下可以同時使用的配置項有:

  • flow_firstHorizontalStyle = "spread|spread_inside|packed" //第一行 Chains 鏈的展示形式
  • flow_firstVerticalStyle = "spread|spread_inside|packed"
  • flow_firstHorizontalBias = "float" //只在 style 爲 packed 時才生效,用於控制第一行在水平方向上的偏移量
  • flow_firstVerticalBias = "float"
  • flow_lastHorizontalStyle = "spread|spread_inside|packed" //最後一行 Chains 鏈的展示形式
  • flow_lastHorizontalBias = "float"

看個例子:

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="cardView1,cardView2,cardView3,cardView4,cardView5"
        app:flow_firstHorizontalStyle="spread_inside"
        app:flow_horizontalGap="30dp"
        app:flow_lastHorizontalBias="1"
        app:flow_lastHorizontalStyle="packed"
        app:flow_verticalGap="30dp"
        app:flow_wrapMode="chain"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

由於 flow_firstHorizontalStyle 值爲 spread_inside,所以首行會往兩側靠邊。由於 flow_lastHorizontalBias值爲 1,所以最後一行也會直接往右靠攏

aligned

此模式和 chain 類似,區別在於不足一行的內容會靠邊對齊顯示

十四、Layer

Layer 作爲一種新的輔助工具,可以在多個視圖上創建一個虛擬的圖層 (layer),和 Flow 不同,它並不會對視圖進行佈局,而是對多個視圖同時進行變換 (transformation) 操作。如果想對多個視圖整體進行旋轉 (rotate)、平移 (translate) 或縮放 (scale) 操作,那麼 Layer 將會是最佳的選擇

在佈局文件中先通過 Layer 引用需要進行變換的所有 View,可以不用對 Layer 進行位置約束

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".LayerActivity">

    <TextView
        android:id="@+id/tv1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#f5ec7e"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv2"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#FF9800"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintStart_toEndOf="@id/tv1"
        app:layout_constraintTop_toBottomOf="@id/tv1" />

    <TextView
        android:id="@+id/tv3"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="#2196F3"
        android:gravity="center"
        android:text="Hello World!"
        app:layout_constraintStart_toEndOf="@id/tv2"
        app:layout_constraintTop_toBottomOf="@id/tv2" />

    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/layer"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:constraint_referenced_ids="tv1, tv2, tv3"
        tools:ignore="MissingConstraints" />

    <Button
        android:id="@+id/btn_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:text="開啓動畫"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

然後在代碼中直接對 Layer 進行動畫操作,這樣其引用到的所有 View 都會進行整體動畫

/**
 * @Author: leavesC
 * @Date: 2020/12/26 22:06
 * @Desc:
 * @Github:https://github.com/leavesC
 */
class LayerActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_layer)
        btn_test.setOnClickListener {
            val layer = findViewById<Layer>(R.id.layer)
            val animator = ValueAnimator.ofFloat(0f, 360f)
            animator.addUpdateListener { animation ->
                val angle = animation.animatedValue as Float
                layer.rotation = angle
                layer.scaleX = 1 + (180 - abs(angle - 180)) / 20f
                layer.scaleY = 1 + (180 - abs(angle - 180)) / 20f
                val translationX = 500 * sin(Math.toRadians((angle * 5).toDouble())).toFloat()
                val translationY = 500 * sin(Math.toRadians((angle * 7).toDouble())).toFloat()
                layer.translationX = translationX
                layer.translationY = translationY
            }
            animator.duration = 6000
            animator.start()
        }
    }

}

此外,Layer 比較有用的一個點就是可以用於設置背景色,以前如果我們想要對某塊區域設置一個背景色的話往往需要多嵌套一層,而如果使用 Layer 的話則可以直接設置,不需要進行嵌套

十五、ConstraintSet

Layer 是對 ConstraintLayout 內的一部分控件做動畫變換,ConstraintSet 則是用於對 ConstraintLayout 整體進行一次動畫變換

ConstraintSet 可以理解爲 ConstraintLayout 對其所有子控件的約束規則的集合。在不同的交互規則下,我們可能需要改變 ConstraintLayout 內的所有子控件的約束條件,即子控件的位置需要做一個大調整,ConstraintSet 就用於實現平滑地改變子控件的位置

例如,我們需要在不同的場景下使用兩種不同的佈局形式,先定義好這兩種佈局文件,其中子 View 的 Id 必須保持一致,View 的約束條件則可以隨意設置。然後在代碼中通過 ConstraintSet 來加載這兩個佈局文件的約束規則,apply 給 ConstraintLayout 後即可平滑地切換兩種佈局效果

/**
 * @Author: leavesC
 * @Date: 2020/12/26 23:02
 * @Desc:
 * @Github:https://github.com/leavesC
 */
class ConstraintSetActivity : AppCompatActivity() {

    companion object {

        private const val SHOW_BIG_IMAGE = "showBigImage"

    }

    private var showBigImage = false

    private val constraintSetNormal = ConstraintSet()

    private val constraintSetBig = ConstraintSet()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_constraint_set)
        //獲取初始的約束集
        constraintSetNormal.clone(cl_rootView)
        //加載目標約束集
        constraintSetBig.load(this, R.layout.activity_constraint_set_big)
        if (savedInstanceState != null) {
            val previous = savedInstanceState.getBoolean(SHOW_BIG_IMAGE)
            if (previous != showBigImage) {
                showBigImage = previous
                applyConfig()
            }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putBoolean(SHOW_BIG_IMAGE, showBigImage)
    }

    fun toggleMode(view: View) {
        TransitionManager.beginDelayedTransition(cl_rootView)
        showBigImage = !showBigImage
        applyConfig()
    }

    //將約束集應用到控件上
    private fun applyConfig() {
        if (showBigImage) {
            constraintSetBig.applyTo(cl_rootView)
        } else {
            constraintSetNormal.applyTo(cl_rootView)
        }
    }

}

十六、ConstraintHelper

Flow 和 Layer 都是 ConstraintHelper 的子類,這兩者都屬於輔助佈局的工具類,ConstraintLayout 也開放了 ConstraintHelper 交由開發者自己去進行自定義

例如,我們可以來實現這麼一種逐步展開的動畫效果

繼承 ConstraintHelper,在 updatePostLayout方法中遍歷其引用的所有控件,然後對每個控件應用 CircularReveal 動畫。updatePostLayout方法會在執行 onLayout 之前被調用

/**
 * @Author: leavesC
 * @Date: 2020/12/26 23:47
 * @Desc:
 * @Github:https://github.com/leavesC
 */
class CircularRevealHelper @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintHelper(context, attrs, defStyleAttr) {

    override fun updatePostLayout(container: ConstraintLayout) {
        super.updatePostLayout(container)
        val views = getViews(container)
        for (view in views) {
            val anim = ViewAnimationUtils.createCircularReveal(
                view, view.width / 2,
                view.height / 2, 0f,
                hypot((view.height / 2).toDouble(), (view.width / 2).toDouble()).toFloat()
            )
            anim.duration = 3000
            anim.start()
        }
    }

}

在佈局文件中引用需要執行動畫的 View 即可

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ConstraintHelperActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="300dp"
        android:layout_height="300dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/icon_avatar" />

    <github.leavesc.constraint_layout.CircularRevealHelper
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="imageView"
        tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>

十七、ImageFilterView

ImageFilterView 是放在 ConstraintLayout 的 utils.widget包下的一個 View,從包名可以猜測 ImageFilterView 只是 Google 官方提供的一個額外的工具屬性的類,和 ConstraintLayout 本身並沒有啥關聯

ImageFilterView 直接繼承於 AppCompatImageView,在其基礎上擴展了很多用於實現圖形變換的功能

屬性 含義
altSrc 用於指定要從 src 變換成的目標圖片,可以依靠 crossfade 來實現淡入淡出
crossfade 設置 src 和 altSrc 兩張圖片之間的混合程度。0=src 1=altSrc圖像
saturation 飽和度。0=灰度,1=原始,2=過飽和
brightness 亮度。0=黑色,1=原始,2=兩倍亮度
warmth 色溫。1=自然,2=暖色,0.5=冷色
contrast 對比度。1=不變,0=灰色,2=高對比度
round 用於實現圓角,以 dimension 爲值
roundPercent 用於實現圓角,取值在 0f-1f 之間,爲 1f 時將形成一張圓形圖片

看個例子。在 xml 中聲明多個 ImageFilterView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ImageFilterViewActivity">

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/imageView1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        app:layout_constraintEnd_toStartOf="@id/imageView2"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/icon_avatar_normal" />

    //省略其它 ImageFilterView

    <androidx.appcompat.widget.AppCompatSeekBar
        android:id="@+id/seekBar"
        style="@style/Widget.AppCompat.ProgressBar.Horizontal"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:max="100"
        android:progress="0"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

在代碼中來調整以上屬性值

/**
 * @Author: leavesC
 * @Date: 2020/12/27 0:17
 * @Desc:
 * @Github:https://github.com/leavesC
 */
class ImageFilterViewActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_image_filter_view)
        seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
                if (fromUser) {
                    val realProgress = (progress / 100.0).toFloat()

                    imageView1.saturation = realProgress * 20
                    imageView2.brightness = 1 - realProgress

                    imageView3.warmth = realProgress * 20
                    imageView4.contrast = realProgress * 2

                    imageView5.round = realProgress * 40
                    imageView6.roundPercent = realProgress

                    imageView7.crossfade = realProgress
                }
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {

            }

            override fun onStopTrackingTouch(seekBar: SeekBar?) {

            }
        })
    }

}

十八、Demo 下載

示例代碼我均已放到 Github,請查收:AndroidOpenSourceDemo

十九、參考資料

一個人走得快,一羣人走得遠,寫了文章就只有自己看那得有多孤單,只希望對你有所幫助😂😂😂

查看更多文章請點擊關注:字節數組

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章