Android ConstraintLayout约束布局完全解析

布局优化是性能优化的一个方向点,包括了根据需求应该选用哪种布局容器、ViewStub懒加载,如何减少布局层级等,今天我们要探讨的就是如何使用ConstraintLayout来优化我们的布局层级。

概述.png

提出问题

  1. 为什么要用这个布局?
  2. 怎么用这个布局?
  3. 不足在哪里?

优势

ConstraintLayout就是为性能而生,目标就是减少布局嵌套,提高measure+layout性能,来看看官方给出的数据。

性能比较结果.jpeg

ConstraintLayout 在测量/布局阶段的性能比 RelativeLayout大约高40%!而它使用的性能检测工具是Android 7.0(API 级别 24)中引入的 OnFrameMetricsAvailableListener。通过该类,你可以收集有关应用界面渲染的逐帧时间信息,进而比较分析不同布局每次测量和布局操作所花费的时间。

另外使用AS的图形化界面可以非常方便的完成布局操作,这里不赘述了文末有参考文章。

配置

引入最新版本constraint layout库

implementation 'com.android.support.constraint:constraint-layout:1.1.3'

文中示例github项目
https://github.com/wanderingGuy/constraintlayout_sample

旧界面转换为ConstraintLayout

首先,AS支持一键将已有的布局文件转成ConstraintLayout,是不是很贴心,要做就做全套的。

布局转换.png

用法

1. 属性layout_constraintXXX_toYYYOf

xxx表示该控件在哪个方向的约束,YYY表示该约束指向的控件的方向(我已经尝试说人话了...),它们的可以是 left/right/top/bottom/start/end的任意一种,包含了水平方向和垂直方向的约束,属性的值为目标控件的id,看个例子item1_constraint.xml。

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

  <TextView
      android:id="@+id/titleTextView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Hello World1!"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

  <TextView
      android:id="@+id/subtitleTextView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Hello World2!"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintTop_toBottomOf="@id/titleTextView" />
</android.support.constraint.ConstraintLayout>
效果图.png

可以看到两个控件水平方向的约束都指向parent也就是父控件ConstraintLayout,由于两边拉力作用就成了居中的样式,垂直方向上由于第二个textview使用了约束app:layout_constraintTop_toBottomOf="@id/titleTextView",即它的顶部约束为在第一个textview的下面,也就形成了效果图的样子。

2. match_constraint

由于ConstraintLayout的设计就是不想出现层次嵌套,这导致传统布局里使用的match_parent也就不适用了。那如何实现match_parent的效果呢?来看一个例子test_match_constraint.xml。

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Button
        android:id="@+id/bt1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button1" />
    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="button1"
        app:layout_constraintLeft_toRightOf="@id/bt1"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/bt1"/>
</android.support.constraint.ConstraintLayout>
效果图2.png

可以看到将第二个button的layout_width设置为0dp后,它并没有不显示出来而是占据的水平方向的剩余空间。没错,在ConstraintLayout中宽高指定为0dp表示的是在满足约束条件下的最大值,也即match_constraint。

3. 宽高比layout_constraintDimensionRatio

不多BB直接看示例test_dimension_ratio.xml

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/banner"
        android:layout_width="300dp"
        android:layout_height="0dp"
        android:text="banner"
        android:gravity="center"
        android:textSize="30sp"
        android:background="@android:color/holo_blue_light"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintDimensionRatio="2:1"/>
</android.support.constraint.ConstraintLayout>

效果图3.png

可以看到设置app:layout_constraintDimensionRatio="2:1属性表示宽高比为2:1,在已经限定控件宽度为300dp时,高度指定为0dp则可自己算出实际高度。

4. radius角度约束

秒懂的属性,直接来看例子test_radius.xml

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按钮"
        app:layout_constraintCircle="@id/fab_menu"
        app:layout_constraintCircleRadius="100dp"
        app:layout_constraintCircleAngle="45"/>
    <Button
        android:id="@+id/fab_menu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tint="#fff"
        android:text="btn1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
效果图4.png

5. bias偏向拉力比例

示例test_bias.xml

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="50dp"
        android:layout_height="wrap_content"
        app:layout_constraintHorizontal_bias="0.8"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />
</android.support.constraint.ConstraintLayout>
效果图5.png

可以看到虽然水平方向都有拉力,但设置了layout_constraintHorizontal_bias属性后产生了偏移,属性值从0到1,0为最左侧1为最右侧,需要注意的是该属性只有水平方向上都有约束才会生效。

6. 链chain

水平或垂直方向上相互约束的两个或更多view组成一个约束链,通常可用于实现底部导航样式,示例test_chain.xml

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Button
        android:id="@+id/bt1"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintHorizontal_weight="2"
        android:text="第一个"
        android:textSize="30sp"
        app:layout_constraintHorizontal_chainStyle="spread_inside"
        app:layout_constraintRight_toLeftOf="@id/bt2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent" />
    <Button
        android:id="@+id/bt2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="第二个"
        android:textSize="30sp"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/bt1"
        app:layout_constraintRight_toLeftOf="@id/bt3"/>
    <Button
        android:id="@+id/bt3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第三个"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toRightOf="@id/bt2" />
</android.support.constraint.ConstraintLayout>

效果图

效果图6.png

其中layout_constraintHorizontal_chainStyle属性可设置一个约束链中view的分布情况。

chain.jpeg

本例结合layout_constraintHorizontal_weight属性实现了weight chain效果。

7. GuideLine参照线

为方便的指定约束的参照对象,ConstraintLayout内部可通过GuideLine来实现,它不会显示在界面中。
示例test_guideline.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <android.support.constraint.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="100dp"/>
    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="btn1"
        android:textSize="30sp"
        app:layout_constraintLeft_toRightOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>

效果图7.png

可以看到通过layout_constraintGuide_begin指定具体偏移值,而通过layout_constraintGuide_percent可指定偏移百分比。

8. 属性goneMargin

当约束对象可见性设置为gone后生效,示例test_gone_margin.xml。

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Button
        android:id="@+id/bnt1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        app:layout_constraintLeft_toLeftOf="parent"
        android:visibility="gone"
        android:text="bnt1"/>

    <Button
        android:id="@+id/bnt2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        app:layout_constraintLeft_toRightOf="@id/bnt1"
        app:layout_goneMarginLeft="200dp"
        android:visibility="visible"
        android:text="bnt2"/>
</android.support.constraint.ConstraintLayout>
效果图8.png

9. 其他

  • layout_constrainedWidth
    针对view宽高为wrap_content时,控件的实际宽高超过了约束条件时应该如何显示,默认是false,示例test_constraint_width.xml。
  • 控件Barrier-- 限定范围,示例test_barrier.xml。
  • 控件Group--引用的组件可批量设置属性,示例test_group.xml。
  • 控件PlaceHolder--占位符,示例test_place_holder.xml。
  • 其他综合练习参照test_common.xml、test_video_constraint.xml。

注意事项

  • 没有约束的layout_marginTop等属性无效。

代码布局

实际开发过程中难免需要使用代码动态布局,整体思路为下面的代码,具体示例参照ProgramActivity.class。

//1.创建约束条件
ConstraintSet set = new ConstraintSet();
//2.从一个ConstraintLayout中克隆出所有的约束条件。
set.clone(layout)
//3.约束条件和constraintLayout绑定
set.applyTo(constraintLayout);

缺点

  • xml文件没有布局层次,可读性差
  • 代码布局复杂性高
  • 约束条件复杂时排查难度大

性能对比

下面是最重要的一趴,ConstraintLayout真的如官方宣传的具备高性能吗?我们使用简单布局和复杂布局两种场景模拟实际开发情况,使用的单元测试为LayoutTest.class,得到如下结果。

简单布局性能比较.png

简单布局表示布局中仅有一层嵌套,上图横坐标表示一次测量和布局的时间,单位为ms,纵轴表示直观上这个简单布局使用哪种传统布局更方便实现,比如我们要实现一个纵向的简单列表,显然线性布局更易实现。通过结果可以看到ConstraintLayout的性能比传统布局的性能差至少一倍。

模拟器--复杂布局性能比较.png

如果说简单布局耗时本来就极少参考意义不大,那复杂布局呢?复杂布局情况下中使用传统布局会导致3-4层的嵌套,符合常见的开发场景,但性能方面仍优于ConstraintLayout,其中FrameLayout的性能表现最好。

结语

关于性能的评测国外的文章也很多,都有得出类似的结论,他们大都归咎于随着ConstrainLayout版本的升级,引入更多的功能而导致性能下降,但博主使用许多更低版本的库测试,结果差别也不大,这就是为什么文章要起这个标题。如果性能没跟上,那由于ConstrainLayout许多缺点的存在,我们完全没有理由选择使用它,期待官方会做进一步优化工作。当然博主后续也会使用其他工具(systrace等)进一步验证此结论。

后记勘误

以上评测使用的是模拟器,笔者也不解为何会出现明显与google大大结果不一致的情况。考虑到真机与模拟器的差异性,笔者使用真机再次验证发现结果与模拟器大相径庭。测试过的真机Android版本为4.4/5.0/6.0。

真机--复杂布局性能比较.png

使用ConstraintLayout作为根布局比其他布局
性能至少提升25%!

至于原因笔者暂时还不清楚,欢迎大神指点迷津。同时对之前评测的结果与得出的结论如有误导已读读者表示十分抱歉。

另有一点遗漏:从1.1版本后官方还提供了一个实验性的优化参数。

app:layout_optimizationLevel

官方文档附上,小伙伴可以尝试。

experimental_optimizer.png

参考文章:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容