约束布局

72 阅读10分钟

0dp 与 match_parent

0dp 表示 在满足约束空间的前提下自定义宽高,需要配合 constraintWidth/Height 使用,默认时会占满剩余约束空间。如下宽度为 0dp 的 view2 占满了剩余的所有空间。在实际使用时 一定要使用 0dp

match_parent 表占满父布局的空间。如果对下图的 view2 设置成 match_parent,那么 view2 将会覆盖住 view1。

image.png

宽高

约束布局中宽高都由约束决定,约束布局中宽高有几种写法:

  1. 具体值
  2. wrap_content:由内容决定且不受约束空间限制。当内容的宽高超过约束空间时,可使用 layout_constrainedWidth 控制其是否可超过约束条件
  3. 0dp:表满足约束空间,在该前提条件下通过 _default 定义默认行为
    • layout_constraintWidth_default:设置 0dp 时的默认行为。wrap 表包裹内容(但会满足约束空间,这是与 wrap_content 最大的区别),spread 表占满约束空间(默认行为),percent 表百分比布局(需要结合 layout_constraintWidth_percent 设置百分比)
  4. 在 chain 中使用权重

layout_constraintWidth

好像是新加的属性,优先级高于 layout_width,汇总了在宽高设定时常见的行为:

  1. match_parent:等于 layout_width=match_parent
  2. wrap_content:等于 layout_width=wrap_content
  3. wrap_content_constrained:等于 layout_width=wrap_content 与 layout_constrainedWidth="true"
  4. match_constraint:layout_width=0dp 默认行为,表占满约束空间

bias

当子 view 未能占满约束定义的全部空间时,bias 可指定左右/上下两个空间的占比。类似于 LinearLayout 的 weight。但要注意 一定要指定双边约束,即上下/左右约束需要同时指定。如下,指定了垂直方向的约束,bias = 0.2,因此 view1 上下部分的占比为 0.2:0.8

    <View
        android:id="@+id/view1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@color/purple_200"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.2" />

GuideLine

设计 guideline 的主要目的是:将常用的定位距离(如页面边距、分界线)抽象成一条参考线,方便多个视图统一使用和后期修改

由于 view 基于 guideline 进行定位,因此对 guideline 执行动画操作时,所有基于它的 view 会同步进行动画

Guideline 并不会绘制,它只是一条参考线。因此 guideline 设置的宽高没有意义。挪到参考线时,所有与参考线建立约束的 view 会同时移动。

通过 orientation 指定参考线是垂直、水平方向,通过 guide_start/end/percent 指定参考线的位置。

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.3" />

chain

当一组视图在 同一轴线上(水平或垂直)通过双向约束连接在一起时,就形成了一个链。也就是说一个链上的元素需要相互约束,例如 A startToEnd B,那么 B 就需要 endToStart A。

链的行为由第一个元素通过 chainStyle 指定,一个链中的元素可以看作一个整体,chainStyle 决定链中元素如何排列布局:

  • packed:当作一个整体,紧密贴合,并居中
  • spread:元素间等距分隔,且链头、链尾与约束边界也参与分配
  • spread_inside:元素间等距分隔,但链头、链尾紧贴约束边界

链中的元素也可分别指定 weight,它们将会 weigth 指定的比例分配约束空间,此时会忽略链头元素指定的 chainStyle。

使用 chain 可以实现跟随布局:

  1. 首先构建成链,chainStyle 要指定成 packed,这样所有的元素才会抱团
  2. 链头指定 bias = 0,这样链才会居左显示
  3. 需要动态调整宽度的 view 要满足两个条件:宽度跟随内容且不能超过约束条件。所以有两种写法:
    • width = 0dp,constraintWidth_default="wrap":前者指定要满足约束条件,后者指定要满足约束条件时的默认行为为 wrap_content
    • layout_width="wrap_content",layout_constrainedWidth="true" 强制指定宽度为 wrap_content 时要满足约束条件

barrier

与 guideline 类似,也是一个虚拟视图。但使用 guideline 时首先定义 guideline 的位置,其余 view 基于 guideline 定位;而 barrier 相反,它首先定位相关的 view,再基于这些 view 定位 barrier,因此它随着 view 变化而移动,永远定位在某个 view 组的最边缘

例如当你有几个尺寸会变化的视图,并且希望另一个视图始终在这些视图的右侧(或下方)时,屏障非常有用。如下:无论 view,view2 谁的宽度比较宽,view3 会始终在它的右边

image.png

常用属性:

  1. barrierDirection:定义 barrier 的位置,如 end 表 view 组的最右侧等
  2. constraint_referenced_ids:定义 barrier 基于哪些 view 进行定位,多个 view 通过逗号分隔
   <!-- 作为约束布局子 view 而存在 -->
    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/layer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="end"
        app:constraint_referenced_ids="view,view2" />

layer

统一对一组 view 执行变换,使用 layer 可以对不同 view 同时执行操作,无论这些 view 是否在同一个 viewGroup 下

使用 constraint_referenced_ids 指定对应的 view id,多个 id 使用逗号分隔

<!--作为 ConstraintLayout 子布局而存在-->
    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/layer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="end"
        app:constraint_referenced_ids="view,view2" />

flow

以 flow 形式管理多个 view,会将 view 构建成 chain,是对 chain 的扩展,也因此 flow 有不少跟 chain 相关的属性。flow 会以行列形式管理

  1. constraint_referenced_ids:指定哪些 view 以 flow 形式管理

  2. flow_wrapMode:指定换行模式

    • none 不换行
    • chain 换行且每行都以 chain 形式管理
    • aligned 换行且每行列元素都对齐,此时设置的所有与 chain 相关的属性都不生效
  3. flow_horizontalStyle 与 flow_firstHorizontalStyle、flow_lastHorizontalStyle:分别设置中间行、第一行、最后一行的 chainStyle,其值与 chain 中的 chainStyle 一样

    • packed:所有 view 都抱团
    • spread: 所有 view 都分散对齐,且不紧靠左右边界
    • spread_inside: 所有 view 分散对齐,但会紧靠左右边界
  4. flow_horizontalBias,flow_firstHorizontalBias 与 flow_lastHorizontalBias: 分别指定中间行、第一行、最后一行的 bias。要注意:它们必须与 style 一起使用,单用无效

image.png

  1. flow_horizontalGap 与 flow_verticalGap: 设置每列/行 view 之间的间隔
  2. flow_maxElementsWrap: 设置每一行最多容纳几个元素

基于 flow 可实现分散对齐效果(限制比较大,只在字数固定时可用)

image.png

ConstraintSet

约束布局的约束条件除了可以在 xml 中定义外,还可以通过代码进行设置,此时就需要使用 ConstraintSet。使用步骤:

  1. clone():先从约束布局中提取所有的约束条件
  2. 设置各种约束条件
    • clear:清除指定 view 的约束条件,可以指定清除哪个方向的约束。同时会清除约束条件以及 margin
    • connect:设置新的约束条件。第一个参数为要修改的 view,后面参数指定它的约束。它们后 xml 中的对应关系是:第二个参数_to第四个参数Of=“第三个参数”,如下面代码中的 connect 就表示 start_toStartOf="parent"
    • setMargin:设置 margin,根据第二个参数决定是 marginStart 还是 marginEnd
  3. applyTo():代码处理完所有的约束条件后,应用到指定的约束布局中
            val set = ConstraintSet()
            val cl = findViewById<ConstraintLayout>(R.id.constraint)
            // 先从布局中读取所有的约束条件
            set.clone(cl)
            
            // 修改约束条件
            set.clear(R.id.tv3, ConstraintSet.BOTTOM)
         
         // 设置 margin
            set.setMargin(R.id.tv3, ConstraintSet.START, 100)
            // 设置新的约束
            set.connect(
                R.id.tv3,
                ConstraintSet.START,
                ConstraintSet.PARENT_ID,
                ConstraintSet.START
            )
            // 应用修改后的约束条件
            set.applyTo(cl)

MotionLayout

常见问题

  • xml 中定义完约束后不生效
    • 可能是因为 Constraint 中没有定义 width 与 height
    • 可能用的全名空间是 app,约束的定义应该全换成 motion
    <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto"
    <!-- 这个 app 应该删除掉不应该出现经 motionlayout 对应的 xml  -->
    xmlns:app="http://schemas.android.com/tools" />

基本介绍

它是约束布局的子类,因此可以使用约束布局的一切属性。

与约束布局最大的不同是 MotionLayout 支持动画。它支持从一个约束视图(即 scene)采用动画形式过渡到另一个视图。比如开始时 A 靠近屏幕左边(即最初 scene),结束时 A 要靠近屏幕右边(即终止 scene),使用 motionlayout 可以自动在两个 scene 之间添加动画。

类似于在应用 ConstraintSet 之前调用 TransitionManager 的beginDelayedTransition(),如下:

// ConstraintSet 切换动画

val bound = ChangeBounds()
bound.duration = 1000
TransitionManager.beginDelayedTransition(layout, bound)
set.clone(layout)
if (flag) {
    set.clear(R.id.tv, ConstraintSet.START)
    set.connect(R.id.tv, ConstraintSet.START, R.id.start, ConstraintSet.END)
    set.connect(R.id.tv, ConstraintSet.END, R.id.end, ConstraintSet.START)
} else {
    set.clear(R.id.tv, ConstraintSet.END)
    set.setMargin(R.id.tv, ConstraintSet.START, 50)
}
set.applyTo(layout)

MotionScene

MotionLayout 需要在 res/xml 文件夹中另定义一个 xml 文件,用来定义动画相关的内容,然后通过layoutDescription="@xml/xx 引用该 xml 文件,一个完整的定义如下:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <!-- 过渡定义 -->
    <Transition
        motion:constraintSetStart="@+id/collapsed"
        motion:constraintSetEnd="@+id/expanded"
        motion:duration="1000"
        motion:interpolator="easeInOut">
        
        <!-- 点击触发器,点击 fab 时触发 -->
        <OnClick
            motion:targetId="@+id/fab"
            motion:clickAction="toggle" />
            
        <!-- 滑动手势 -->
        <OnSwipe
            motion:touchAnchorId="@+id/header"
            motion:touchAnchorSide="top"
            motion:dragDirection="dragDown" />
            
        <!-- 关键帧 -->
        <KeyFrameSet>
            <KeyPosition
                motion:motionTarget="@id/xx"
                motion:framePosition="50"
                motion:keyPositionType="parentRelative"
                motion:percentX="0.8"
                motion:percentY="0.2" />
            <!-- position 可以定义位置,attribute 可以定义别的属性 -->
            <!-- 如 alpha 等 -->
            <KeyAttribute
                motion:framePosition="75"
                android:rotation="180" />
        </KeyFrameSet>
    </Transition>

    <!-- 折叠状态 -->
    <ConstraintSet android:id="@+id/collapsed">
        <Constraint
            android:id="@+id/fab"
            android:layout_width="56dp"
            android:layout_height="56dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            android:layout_margin="16dp">
            
            <!-- 除常规的位置、大小等属性外,montionLayout 还支持修改透明度等 -->
 <!-- 这些属性的修改就需要通过 CustomAttribute 定义-->
            <CustomAttribute
                motion:attributeName="text"
                motion:customStringValue="你好个屁" />
        </Constraint>
    </ConstraintSet>

    <!-- 展开状态 -->
    <ConstraintSet android:id="@+id/expanded">
        <Constraint
            android:id="@+id/fab"
            android:layout_width="120dp"
            android:layout_height="56dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            android:layout_margin="16dp">
            
            <CustomAttribute
                motion:attributeName="backgroundColor"
                motion:customColorValue="#FF5722" />
        </Constraint>
    </ConstraintSet>

</MotionScene>
  • OnClick:定义点击某个 view 时触发动画,也可通过代码控制。clickAction 定义点击时的行为
    • toggle:默认值,向另一个状态切换,比如当前是 start 状态,则点击后会切换到 end 状态。 如果当前正在执行切换,也会停止切换并向另一个状态切换
    • transition/jump:切换到固定的状态。transition 会带动画,jump 不带
    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="1000">
        <OnClick
            motion:clickAction="toggle"
            motion:targetId="@+id/mClick" />
    </Transition>
  • OnSwipe:指定响应 MotionLayout 滑动的 View

    • touchAnchorId/touchAnchorSide: 手指拖动时需要确定当前动画的进度,这两个属性就是用于告诉 MotionLayout 如何确定当前进度。它会根据 id 指定的 view 的边(由 side 指定)的起始、终止位置做为运动总距离,并结合手指的移动的距离计算出动画进度。
    • touchRegionId: 指定 view,该 view 发生 swipe 事件时会触发 anchor 执行动画;如果未指定,则默认为整个 motionlayout
    • dragDirection: 指定响应的方向,如向上、左、右等
    • onTouchUp: 当手指抬起时 view 如何处理。比如回到起始位置或者终止位置等
  • CustomAttribute:约束中只能定义位置、大小相关的属性,如果想定义透明度等属性(比如背影色、透明度、文字等)就需要在 Constraint 内部使用 CustomAttribute 节点,它的使用方法可阅读 ConstraintAttribute::parse() 方法。对于一个属性的修改至少有两个前置条件:

    • 如何修改,即想修改该属性需要调用 View 的哪个方法。可以通过 attributeName 或 methodName 指定修改该属性时需要调用的方法
    • 目标值,根据属性值的类型选择使用不同的 customXXXValue 指定目标值。如完整示例中想修改文本,就需要使用 customStringValue 指定目标值
  • KeyFrameSet 定义动画中的关键帧

    • keyposition:定义动画中某个时刻 view 的位置,要注意: 定义的位置并不是 view 真正经过的位置,而是控制运动轨迹的控制点,运动轨迹是贝塞尔曲线形式的
    • KeyAttribute: 定义动画中某个时刻 view 的透明度等其它属性
    <KeyFrameSet>  
     <KeyAttribute
         motion:motionTarget="@id/button"  
         motion:framePosition="80"  
         android:alpha="0.5"  
         android:rotationX="45"  
         android:scaleX="2"  
         android:scaleY="2"  
         android:translationZ="20dp"/>  
    </KeyFrameSet>
    
    • KeyCycle: 用于重复性、周期性的动画想着的设置。比如周期性地透明度变化,左右摇晃等