Android ConstraintLayout使用实践

ConstraintLayout使用实践

ConstraintLayout是2017年Google IO上推出的一种新的布局方式,相较于其他几种布局(LinearLayout,RelativeLayout等),ConstraintLayout设计用于构造扁平的布局层级,减少layout的嵌套,从而提升界面绘制的速度,在布局中使用ConstraintLayout性能比其他几种布局会提升很多,后面我们会详细介绍到。

Why ConstraintLayout?

性能优势

关于view的性能问题,我们首先应该要了解Android是如何绘制view的,View的绘制主要分为如下三个步骤:

1.Measure过程

系统在绘制整个view层级时是依次从上到下绘制的,第一个需要确定的是view的大小,如果是ViewGroup,在确定其大小之后还需要再测量其子view的大小

2. Layout过程

当view树中所有的view大小确定完毕后,系统开始确定每个view所处的位置,这个过程称为layout

3. Draw过程

系统绘制的最后一步也是从上到下,每一个view的onDraw方法中的Canvas对象包含了绘制的基本信息,这个canvas传给GPU然后根据第一二步确定的大小和位置来确定最终的view形态


View Tree绘制过程
图1:View Tree绘制过程

从上图可知,每一个完整的view树的绘制过程都是从上到下,这就意味着,如果我们布局中嵌套的层级越多,在绘制中所需要的时间和内存空间就越多,性能也就越差。

拿一个官方的例子来说:


布局Demo
图2:布局Demo

如上图,如果完成一个如上所示的布局,按照以前的布局来设计的话,可能大致是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<RelativeLayout>
<ImageView />
<ImageView />
<RelativeLayout>
<TextView />
<LinearLayout>
<TextView />
<RelativeLayout>
<EditText />
</RelativeLayout>
</LinearLayout>
<LinearLayout>
<TextView />
<RelativeLayout>
<EditText />
</RelativeLayout>
</LinearLayout>
<TextView />
</RelativeLayout>
<LinearLayout >
<Button />
<Button />
</LinearLayout>
</RelativeLayout>

为了对比使用ConstraintLayout而产生的性能差异,我们通过Systrace工具来监测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
python $ANDROID_HOME/platform-tools/systrace/systrace.py --time=20 -o ~/trace.html gfx view res
gfx - Graphics
input - Input
view - View System
webview - WebView
wm - Window Manager
am - Activity Manager
sm - Sync Manager
audio - Audio
video - Video
camera - Camera
hal - Hardware Modules
res - Resource Loading
dalvik - Dalvik VM
rs - RenderScript
bionic - Bionic C Library
power - Power Management
pm - Package Manager
ss - System Server
database - Database
network - Network
adb - ADB
vibrator - Vibrator
aidl - AIDL calls
pdx - PDX services
sched - CPU Scheduling
freq - CPU Frequency
idle - CPU Idle
disk - Disk I/O
sync - Synchronization

通过运行如上命令,会生成一个trace.html的文件,通过浏览器打开可以得到系统监测的一些数据


传统布局Systrace监测结果
图3:传统布局Systrace监测结果

我们可以看到在alert选项菜单中产生了89个绘制警告。

现在我们采用ConstraintLayout来实现同样的效果,代码大致是这样

1
2
3
4
5
6
7
8
9
10
11
12
<android.support.constraint.ConstraintLayout>
<ImageView />
<ImageView />
<TextView />
<EditText />
<TextView />
<TextView />
<EditText />
<Button />
<Button />
<TextView />
</android.support.constraint.ConstraintLayout>

将布局采用ConstraintLayout替换之后,我们再用systrace监测,或得到如下结果


ConstraintLayout布局Systrace监测结果
图4:传统布局Systrace监测结果

可以看到view绘制的警告数有大幅度减少。

除了通过systrace跟踪性能问题,在android 7.0(Api 24)及之后版本中加入了帧绘制的
监听OnFrameMetricsAvailableListener,这个监听器可以在回调中接收到系统每一帧
绘制的时长参数

1
window.addOnFrameMetricsAvailableListener(frameMetricsAvailableListener, frameMetricsHandler);

这里,我们需要检测view的测量时间,所以如下代码

1
2
3
4
5
Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ ->
val frameMetricsCopy = FrameMetrics(frameMetrics);
// Layout measure duration in nanoseconds
val layoutMeasureDurationNs = frameMetricsCopy.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION)
}

通过多次跟踪最后的出的综合平均数据可如下图所示


性能对照表
图5:性能对照表

最终的到的数据结果为:使用ConstraintLayout布局比使用传统的布局方式measure/layout时间大致缩短了40%

以上数据在 Nexus 5X的设备,Android 8.0及ConstraintLayout 1.0.2版本上监测

使用实践

相对位置布局(Relative positioning)

相对位置布局的方式跟RelativeLayout布局类似,根据控件与控件之间的相对位置来固定控件的位置

控件与控件之间相对位置从方向上来看可以做如下划分

  • 水平方向:left,right,start和end(start和end作用等同于left和right,但是推荐用start和end,因为系统方向配置有时候是RTL-Right To Left,阿拉伯国家的阅读方向是从右至左的,所以start和end会自动转换)

  • 垂直方向:top,bottom和text baseline(顶部和底部都好理解,这里baseline需要说明一下,align_baseline用作内容对齐,比方说TextView中的文字内容对齐)



图6:相对位置示例

如上图ButtonB相对于ButtonA的右侧

1
2
3
<Button android:id="@+id/buttonA" ... />
<Button android:id="@+id/buttonB" ...
app:layout_constraintLeft_toRightOf="@+id/buttonA" />

layout_constraintLeft_toRightOf这个属性表示将buttonB的左边放置于buttonA的右边



图7:相对位置参考属性

上图列出了控件位置信息,如下列表是配置相对位置的具体xml属性

  • 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

以上属性接收的参数除了是其他控件的id外,还可使用parent表示相对于父控件,如:

1
2
<Button android:id="@+id/buttonB" ...
app:layout_constraintLeft_toRightOf="parent" />

控件Margin设置(Margins)

当控件相对位置设置好了,通常我们需要设置控件与控件的距离,这里就需要设置Margin,margin属性与我们在其他Layout,如RelativeLayout中的margin设置功能是一样的,这里需要注意的是ConstraintLayout中的margin设置只接受0或正数数值或引用dimension中的数值,不接受负数的margin,设置了负数默认都会当成0处理

  • android:layout_marginStart
  • android:layout_marginEnd
  • android:layout_marginLeft
  • android:layout_marginTop
  • android:layout_marginRight
  • android:layout_marginBottom
控件设置为Gone后相对控件的margin设置

还是控件A与控件B的例子,控件B在控件A的左边,这时如果设置控件A消失,我们设置控件A的visibility为gone,控件A消失了,这时控件B就左移到了原先控件A的位置,但是控件B的左边距要跟控件A原先的左边距保持一样才是正确的,这时便可以用到如下的goneMargin属性:

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

居中设置(Centering positioning)

1
2
3
4
5
<android.support.constraint.ConstraintLayout ...>
<Button android:id="@+id/button" ...
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent/>
</>

当一个控件同时限制左右位置或上下位置与同一个控件或者parent时,此控件会处在横向空间或者垂直控件中间,居中显示

Bias

有时候我们的需求中会遇到控件在中间位置靠左百分之多少的位置或靠上百分之多少的位置,这个时候就可以使用bias属性

  • layout_constraintHorizontal_bias 横向位置的偏离率
  • layout_constraintVertical_bias 垂直位置的偏离率

举个例子

1
2
3
4
5
6
<android.support.constraint.ConstraintLayout>
<Button android:id="@+id/button"
app:layout_constraintHorizontal_bias="0.3"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent/>
</android.support.constraint.ConstraintLayout>

效果如下图所示,当控件居中时bias是0.5,设置为0.3,表示控件距离作为30%的距离,使用此种方式可以更好的解决因屏幕尺寸大小不一样带来的适配或者旋转屏幕来带的尺寸改变的问题



图7:Bia偏离设置

环形相对位置(Circular positioning)



图7:环形需求demo

在ConstraintLayout 1.1及以后版本,加入了环形相对位置,通过设置环形相对的角度及相对半径即可

  • layout_constraintCircle : 相对一另一个控件的Id
  • layout_constraintCircleRadius : 与相对控件圆心的距离
  • layout_constraintCircleAngle :与相对控件的角度(参数从0到360)


图8:环形相对位置

1
2
3
4
5
<Button android:id="@+id/buttonA" ... />
<Button android:id="@+id/buttonB" ...
app:layout_constraintCircle="@+id/buttonA"
app:layout_constraintCircleRadius="100dp"
app:layout_constraintCircleAngle="45" />

效果如下图所示



图9:A,B控件环形位置设置

控件可见性以及对其他控件的影响(Visibility behavior)

ConstraintLayout对于控件的visibility设置为View.GONE后有一些特殊的处理需要我们注意

当我们将控件设置为gone后,控件将不再展示,但是通过控件获取的宽高值跟之前没有变化,有如下两点需要注意:

  • 当整个view布局完成后,被设置成gone的控件宽高可以看成是0(可以理解成一个透明的点)
  • 当设置成gone的控件对其他控件或者父布局有margin,这些margin会被清除当成0


图10:Visibility Gone

如上图所示,控件A gone掉之后A距左的margin也被清除,这样比方说在做一些动画时如,控件A消失,控件B左移到控件A的位置,不应该还留有控件A的左边距距离

控件尺寸的设置(Dimension constraints)

ConstraintLayout长宽尺寸限制

在ConstraintLayout中,我们可以设置其最大最小宽高,当ConstraintLayout的宽高是WRAP_CONTENT时,如下属性便可以起作用

  • android:minWidth 设置ConstraintLayout的最小宽度
  • android:minHeight 设置ConstraintLayout的最小高度
  • android:maxWidth 设置ConstraintLayout的最大宽度
  • android:maxHeight 设置ConstraintLayout的最大高度
控件的长宽尺寸限制

控件长宽限制通过android:layout_widthandroid:layout_height来定义,如要有如下三种形式:

  • 指定长宽,如100dp或者R.dimen.button_width
  • 使用WRAP_CONTENT,通过控件自己的内容来计算长宽
  • 使用 0dp等同于MATCH_CONSTRAINT撑满父布局


图11:Margin

前两种方式跟android中其他Layout一样,但是第三种有区别,ConstraintLayout中不建议使用MATCH_PARENT,通过使用0dp即可

WRAP_CONTENT属性版本的变动

在ConstraintLayout 1.1版本之前,如果控件的宽高属性设置为WRAP_CONTENT,ConstraintLayout在计算宽高是不会将控件的margin等限制考虑进去,也就是说控件可能超出屏幕。1.1以及后版本中增加了如下属性用来控制WRAP_CONTENT的控件也将margin的constraint作为布局计算

  • app:layout_constrainedWidth=”true|false”
  • app:layout_constrainedHeight=”true|false”

MATCH_CONSTRAINT属性

match_constraint这个属性跟其他layout中的match_parent类似,默认是占满所有可用空间,如下属性可用来对控件长宽做一些其他限制

  • layout_constraintWidth_min 和 layout_constraintHeight_min : 设置控件的最小宽高
  • layout_constraintWidth_max 和 layout_constraintHeight_max : 设置控件的最大宽高
  • layout_constraintWidth_percent 和 layout_constraintHeight_percent : 设置控件占据父控件的百分比作为此控件的宽高

设置宽高的最大最小值,可以设置具体的dp为单位的数值,也可以设置为“wrap”属性,其效果与设置为wrap_content一致

设置百分比的宽高时有如下限制:

  • layout_width或layout_height需设置为0dp(即MATCH_CONSTRAINT)
  • 如果设置了宽高的default值,那么default值必须也是百分比app:layout_constraintWidth_default=”percent”或者app:layout_constraintHeight_default=”percent”(此限制在1.1-beta1和1.1-beta2是强制的,在接下来的版本中不是强制的)
  • layout_constraintWidth_percent或layout_constraintHeight_percent的范围 0到1之间,包含0,1
宽高比

在ConstraintLayout中,我们可以设置控件为固定的宽高比,如下例子所示:

1
2
3
<Button android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1" />

在使用固定宽高比时,至少宽高中有一个的尺寸被设置为0dp(match_constraint),上面的例子button的高度总是与宽度一直,宽高比为1:1

layout_constraintDimensionRatio这个属性可接受一个float的浮点数值,及 width除以height得出的数,也可以使用一个比例表达式“width:height”

当控件宽高都是match_constraint(0dp)时,可以通过在layout_constraintDimensionRatio属性值中指定宽高来作为调整项,如下例子:“H,16:9”表示宽度固定充满父布局,高度为宽度的9/16,同理也可设置为
“W,16:9”

1
2
3
4
5
<Button android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="H,16:9"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>

链式布局(Chains)

链式布局主要用于在垂直或水平方向上一组控件的统一模式处理

创建链

当一组控件通过双向链接constrained即表示这一组控件是链式布局,如下图实例所示



图11:链

链式头

链式布局中水平方向最左边的或者垂直方向最上面的那个控件即为链式头,链式头控制着整个
链式布局,即在链式头中的链式属性作用于整个链式布局

链的种类

链式布局通常用在我们多个控件平分横向或者垂直方向控件,但是控件分布的类型有多种
这里主要有5中链式类型,通过在链式头中设置 layout_constraintHorizontal_chainStyle或者layout_constraintVertical_chainStyle属性,具体的种类如下图所示



图12:Chain Style

CHAIN_SPREAD
此种类型表示控件在可用空间完全展开,n个控件隔离出n+1个空间,chainStyle在未设置情况下
默认是此种类型

CHAIN_SPREAD_INSIDE
此种类型与第一种类似,但是链的首部和尾部紧贴空间边缘,未隔离,n个控件隔离出n-1个空间

Weighted chain
当链式中的空间是match_constraint,此种模式下的chain通过设置控件的layout_constraintHorizontal_weight或者
layout_constraintVertical_weight来设置权重,这个跟LinearLayout中控件的weight属性类似

CHAIN_PACKED
此种模式下,链式布局中的控件紧挨着,默认居中显示,当设置了layout_constraintHorizontal_bias或者
当设置了layout_constraintVertical_bias时,控件就会做指定bias百分不的距离偏移

链内margin设置

如果链式布局中的控件设置了margin,那个margin的距离加载原先链式隔离的空间上,举例来说,A,B,C三个Button横向spread链式布局,如果A设置rightMargin为10dp,那么A跟B的间距除了原先距离还要再加上这10dp.
Button的margin将与button作为一个整体来计算控件的链式分割的距离

虚拟辅助对象(Virtual Helpers objects)

辅助对象主要是用来更好的构建布局,在Android Studio的Layout Editor右键单击布局区域便可以唤出选项菜单



图13:Guideline

Guideline

Guideline类似于ps或者sketch中的辅助标线,从上图可知可以创建横向的或者竖直的辅助线,Guideline有三种标尺模式:距左(距顶),距右(距低),百分比,分别在竖直标线的顶部或横向标线的左边有个原型按钮,通过点击此按钮来进行切换。距左(距顶)模式可以设置标线距离左边指定距离,距右(距低)同理,而百分比则表示标线从左至右的百分比距离,如50%则表示居中。选择哪种模式可以根据自己的喜好。



图12:AndroidStudio Layout Editor

如上图示例,分别建立了竖直和水平的guideline并将其置于50%及中间处,button控件的右边和底部通过对guideline做constraint限制

Barrier

Barrier中文可译为界线的意思,根据意思可以理解这个barrier对象用来将控件与控件之间
做隔离,举一个具体的例子来说明下



图13:Barrier样例

如上图所示,我们加入如上图所示布局的UI,布局左边上面我们定义为text1,左边下边的我们定义为textView2,右边的我们定义为text3,textView3的左边依赖于text1的右边,一切看上去都没有问题。但是有一天我们发现text2的内容比较多时,超过了text1的长度,text2会跟text3重叠,产生这个问题的原因在于text3只与text1做了关联,没有与text2做关联,从而导致了内容重叠。

解决这个问题我们最常见的做法是新建一个orientation为vertical的LinearLayout,将text1与
text2放入LinearLayout,然后LinearLayout与text3关联。但这无疑增加了布局的复杂性,ConstraintLayout可以通过barrier来解决此问题。

通过新建一个vertical barrier,设置barrierDirection为end,barrierDirection表示barrier放置在依赖
对象的哪个方向。接下来将text1与text2通过constraint_referenced_ids属性设置为依赖对象,这样barrier就
放置在了text1与text2的右边,最后我们只需要将text3的左边依赖barrier就解决了问题

Group

Group虚拟对象用于方便处理多个控件的Visibility属性,我们在开发过程中又遇到比方说打开或关闭某个开关,ui中的多个控件消失或显示,通过ConstraintLayout我们只需要将需要这样操作的空间放到同一个Group下,对Group操作即可,Group并不是一个新的View,单纯只是对多个控件对象的引用集合

Layer

Layer虚拟对象与Group类似,也是对多个控件集合操作,只是Layer是统一操作控件变化,如放大缩小,旋转,动画等,读者可自行尝试

优化器(Optimizer)

在ConstraintLayout 1.1及后续版本,开发者可通过app:layout_optimizationLevel属性对ConstraintLayout进行优化

  • none : 不采用任何优化
  • standard :如果我们不指定优化,默认就是standard模式,此模式默认做direct和barrier的优化
  • direct : 对于固定位置的元素优化,如边界线和Guideline
  • barrier : 通过找到布局中使用的barrier,如果可能使用更简单的constraint
  • chain : 优化链式布局(experimental)
  • dimensions : optimize dimensions measures (experimental), reducing the number of measures of match constraints elements

layout_optimizationLevel可接受多个level值,如:app:layout_optimizationLevel=”direct|barrier|chain”

写得好,就打赏一下吧!