深入分析ConstraintLayout的原理及应用场景

2,845 阅读6分钟

前言

ConstraintLayout拥有许多优势,是目前谷歌官方推荐的根布局控件,也是很多大型项目面对复杂布局时首选的控件。本文即深入分析ConstraintLayout的原理及应用场景,让你在开发过程中或面对大厂面试官时得心应手。

概念

谷歌官方文档中有这样一段话,很好地阐述了ConstraintLayout的含义:

ConstraintLayout allows you to create large and complex layouts with a flat view hierarchy (no nested view groups). It's similar to RelativeLayout in that all views are laid out according to relationships between sibling views and the parent layout.

即:ConstraintLayout可让您使用扁平视图层次结构(无嵌套视图组)创建复杂的大型布局。它与RelativeLayout 相似,其中所有的视图均根据同级视图与父布局之间的关系进行布局。

作用

  • ConstraintLayout是约束布局,解决布局嵌套过多的问题,优化布局页面的渲染时间。

和RelativeLayout的区别

既然官方文档强调ConstraintLayout与RelativeLayout类似,那么它们的区别是什么呢?

  • RelativeLayout是相对布局,控件的具体位置是根据控件之间的相对位置计算的。而ConstaintLayout是约束布局,控件之间、控件与父布局之间具有约束关系,控件的位置是按照约束来计算的。

看到这里可能会有人有疑惑,那这相对布局和约束布局看起来不是一样的吗?

当然不是。

ConstraintLayout的约束关系,使它具备了另一个特点,那就是可以添加引导线来辅助布局.所有布局都可以在界面上通过拖动完成。如图。

ConstraintLayout移动控件.png

点击layout布局后,选择图示右上角的Design按钮,即可拖动控件。

而RelativeLayout并不具备这一特点,这也让ConstraintLayout的布局调整更为方便、快捷。

如果此时非ConstraintLayout布局,在上图左方,右键点击最上部分的父容器,选择Convert XXX to Constraintlayout即可转化为约束布局。

ConstraintLayout布局转化.png

  • RelativeLayout、ConstraintLayout都可以通过LayoutParams动态新建布局,即在代码中控制控件尺寸和位置,而不只是在xml文件中设置静态的布局。但是,ConstraintLayout引入了一个新的类,即ConstraintSet,使它可以实现动画效果,对控件的控制能力也更加强大,这是RelativeLayout不具备的能力。
  • 性能上,ConstraintLayout的渲染速度比RelativeLayout更快。 原因:

在布局上RelativeLayout内比ConstraintLayout多了一层ViewGroup,如这样一种情况,屏幕分成两半,左半部分只有一个button,右半部分从上到下有多个button,左边的button需要居中对齐右边,那么便需要将右半部分的多个button用一个viewgroup包裹起来。这样便多了一层嵌套,渲染时间增加。

值得注意的是,LinearLayout的渲染速度也快于RelativeLayout,原因:因为RelativeLayout通过在水平、垂直方向对另一控件的依赖,来计算自身的位置,因此会执行两遍measure,而LinearLayout只需测量一次。ConstraintLayout是RelativeLayout的升级版,LinearLayout的渲染速度同样快于ConstraintLayout。

因此对于层次不深的简单布局,优先使用LinearLayout。

这里可能会有人有疑惑,那层次深与不深又怎么划分呢?三层、四层算深还是五层、六层?

个人的想法是,如果布局仅有一层,不需嵌套,直接使用LinearLayout;如果嵌套多于一层,而根布局需要采用RelativeLayout或ConstraintLayout时,可以直接采用ConstraintLayout。

ConstraintLayout使用条件与场景

使用条件

  • ConstraintLayout布局内的控件必须有水平方向和垂直方向的约束,来表示与父布、兄弟控件的连接或对齐。
  • 水平方向上,start和end为一组。
  • 垂直方向上,top和bottom为一组。 约束如下。
//与父布局底部对齐(parent可改为其他控件ID,即与其他控件底部对齐,下同)
app:layout_constraintBottom_toBottomOf="parent"

//与父布局顶部对齐
app:layout_constraintTop_toTopOf="parent"

//与父布局左端对齐
app:layout_constraintStart_toStartOf="parent"

//与父布局右端对齐
app:layout_constraintEnd_toEndOf="parent"

//在父布局的上方(看属性最右边的元素,即Top,下同)
app:layout_constraintBottom_toTopOf="parent"

//在父布局的下方
app:layout_constraintTop_toBottomOf="parent"

//在父布局的右方
app:layout_constraintStart_toEndOf="parent"

//在父布局的左方
app:layout_constraintEnd_toStartOf="parent"

使用前导包

  1. 在顶级build.gradle文件中
repositories {
    google()
}
  1. 模块级build.gradle
// 尽可能地下载最新版本,如果不确定最新版本号,可以先写入1.0.0,系统会标注提醒最新版本号.
dependencies {
    implementation "androidx.constraintlayout:constraintlayout:2.0.4"
}

使用场景

  • 场景a:A控件与“222”控件居中对齐,且“222”控件在A控件下方。如图(即基于某控件的一边,居中对齐)

约束布局如图.png

代码实现如下:

<?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=".MainActivity">

 <TextView
        android:id="@+id/tv1"
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:gravity="center"
        android:text="A"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

//因为要位于下方,所以使用layout_constraintTop_toBottomOf属性;
//同时左右两边要基于另一控件对齐(因为要居中)
    <TextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="222"
        app:layout_constraintStart_toStartOf="@id/tv1"
        app:layout_constraintEnd_toEndOf="@id/tv1"
        app:layout_constraintTop_toBottomOf="@id/tv1"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 居中用法引申:同一维度(上下或左右)的两个方向同时出现,且相对于父布局对齐。
  2. 非居中用法引申:若不居中对齐,而是基于某一边对齐,只需去掉同一个维度的某一个方向。例如,上方代码中,去掉app:layout_constraintEnd_toEndOf="@id/tv1", 即实现A与“222”控件左方对齐,且A在“222”控件上方。
  • 场景2:六个控件在布局中以三行三列形式分布,且行均分布局高度,列均分布局宽度。如图。

ConstraintLayout场景2.png

核心实现流程如下:

  1. 每一个相对的控件,都要写出相约束的属性(比如layout_constraintEnd_toStartOf;layout_constraintStart_toEndOf)。
  2. 每个控件四个方向的约束位置都要写出来。
  3. 每一个横向或竖向位置的两端,必须与parent相对。
// 横向:
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"// 竖向:
app:layout_constraintTop_toTopOf="parent";
app:layout_constraintBottom_toBottomOf="parent"
  1. 利用权重实现均分(weight属性)
  2. 均分时,长或宽都必须为0。

代码如下所示。

<?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=".MainActivity">
    
/*
核心代码
*/
    <Button
        android:id="@+id/btn1"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn1"
        app:layout_constraintBottom_toTopOf="@id/btn4"
        app:layout_constraintEnd_toStartOf="@+id/btn2"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_weight="1" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn2"
        app:layout_constraintBottom_toTopOf="@id/btn5"
        app:layout_constraintEnd_toEndOf="@id/btn3"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@+id/btn1"
        app:layout_constraintTop_toTopOf="@+id/btn1"
        app:layout_constraintVertical_weight="1" />

    <Button
        android:id="@+id/btn3"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn3"
        app:layout_constraintBottom_toTopOf="@id/btn6"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@+id/btn2"
        app:layout_constraintTop_toTopOf="@+id/btn2"
        app:layout_constraintVertical_weight="1" />

    <Button
        android:id="@+id/btn4"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn4"
        app:layout_constraintBottom_toTopOf="@id/btn7"
        app:layout_constraintEnd_toStartOf="@+id/btn5"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn1"
        app:layout_constraintVertical_weight="1" />

    <Button
        android:id="@+id/btn5"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn5"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintBottom_toTopOf="@id/btn8"
        app:layout_constraintEnd_toEndOf="@id/btn6"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@+id/btn4"
        app:layout_constraintTop_toBottomOf="@id/btn2"
        app:layout_constraintTop_toTopOf="@+id/btn4"
        app:layout_constraintVertical_weight="1" />

    <Button
        android:id="@+id/btn6"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn6"
        app:layout_constraintBottom_toBottomOf="@id/btn5"
        app:layout_constraintBottom_toTopOf="@id/btn9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@+id/btn5"
        app:layout_constraintTop_toBottomOf="@id/btn3"
        app:layout_constraintVertical_weight="1" />

    <Button
        android:id="@+id/btn7"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn7"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="@id/btn8"
        app:layout_constraintEnd_toStartOf="@+id/btn8"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn4"
        app:layout_constraintVertical_weight="1" />

    <Button
        android:id="@+id/btn8"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn8"
        app:layout_constraintBottom_toBottomOf="@id/btn7"
        app:layout_constraintEnd_toEndOf="@id/btn9"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@+id/btn7"
        app:layout_constraintTop_toBottomOf="@id/btn5"
        app:layout_constraintTop_toTopOf="@+id/btn7" />

    <Button
        android:id="@+id/btn9"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:text="btn9"
        app:layout_constraintBottom_toBottomOf="@id/btn8"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toEndOf="@+id/btn8"
        app:layout_constraintTop_toTopOf="@+id/btn8"
        app:layout_constraintVertical_weight="1" />

</androidx.constraintlayout.widget.ConstraintLayout>
  • 场景3:ConstraintSet的动画效果实现 代码实现如下。
ConstraintSet constraintSet=newConstraintSet();//创建ConstraintSet

Button button=newButton(MainActivity.this);//在其中添加一个Button                                         
//此处的constraintLayout为布局中ConstraintLayout的一个id
constraintLayout.addView(button);                   
constraintSet.clone(constraintLayout);                  

constraintSet.constrainWidth(button.getId(),ConstraintLayout.LayoutParams.WRAP_CONTENT);                     
constraintSet.constrainHeight(button.getId(),ConstraintLayout.LayoutParams.WRAP_CONTENT);                     

constraintSet.connect(button.getId(),ConstraintSet.END,ConstraintSet.PARENT_ID,ConstraintSet.END);                     
constraintSet.connect(button.getId(),ConstraintSet.START, 
ConstraintSet.PARENT_ID,ConstraintSet.START);
//这个按钮距离顶部的margin值为1000                     
constraintSet.connect(button.getId(),ConstraintSet.TOP,ConstraintSet.PARENT_ID,ConstraintSet.TOP,1000);                     
constraintSet.connect(button.getId(),ConstraintSet.BOTTOM,ConstraintSet.PARENT_ID,ConstraintSet.BOTTOM);                    
constraintSet.applyTo(constraintLayout);

根据上述代码,ConstraintSet通过如下流程完成布局中增加一个按钮的效果:

  1. 创建ConstraintSet对象;
  2. clone(复制原布局中ConstraintLayout参数);
  3. 设置constrainWidth与constrainHeight(设置新控件宽高是wrap_content还是match_parent);
  4. 调用connect方法(设置新控件与其他控件的约束关系);
  5. applyto(将约束关系应用于新控件与原constraintLayout之间)

更多技术文章欢迎关注公众号度熊君。