尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android布局优化:四种布局原理与ConstraintLayout选型实战

Android布局优化:四种布局原理与ConstraintLayout选型实战 开头做Android开发的谁还没跟布局资源打过几回交道。刚入行那会儿我一度以为布局就是把XML标签往res/layout里一堆能跑就行。直到后来重构一个老项目被布局层级里七八层嵌套的LinearLayout搞得性能崩盘、问题定位到抓狂才真正意识到布局资源不是堆控件而是一门需要认真对待的功课。这篇内容我想围绕布局资源里最常用的四种类型——LinearLayout、RelativeLayout、FrameLayout、ConstraintLayout把它们的原理、适用场景、实操写法以及我踩过的坑一次性讲清楚。不管你是刚接触Android两三周的新手还是已经写过几百个界面但一直靠复制粘贴改属性糊弄的初中级开发这篇应该都能帮你把布局这块地基重新夯实一遍。这四个布局几乎覆盖了日常开发里九成以上的界面场景。搞懂它们各自擅长解决什么问题、在什么情况下选哪个、怎么写才有更好的渲染性能比背一百条属性名更有价值。下面我从它们在工程里的定位开始一个个拆开讲。1. 布局资源在Android工程里的真实位置1.1 资源体系res/layout到底管什么很多新手对布局资源这个概念是模糊的。Android工程里有一个res目录底下按类型分了drawable、mipmap、values、layout等等。layout就是专门存放界面结构定义的地方里面每个XML文件描述的就是一个界面的骨架。布局资源本质上是一份声明式的界面描述文档。你不在Java或Kotlin代码里一行行去new控件、手动计算坐标而是用XML把控件之间的父子关系和排列规则写清楚系统在运行时负责解析这份XML、创建对应的View树。这个设计最大的好处是界面逻辑与业务代码解耦。改布局的时候不用动Java/Kotlin代码调整视觉结构的时候也不用重新编译逻辑层维护成本直接降一个量级。不过也正因为是声明式很多人在学布局的时候容易陷入只背属性、不理解机制的误区。见到layout_width就写match_parent见到layout_weight就直接给1至于为什么这样写、控件之间是怎么测量和摆放的完全不管。这样的代码在简单页面上没事一旦遇到复杂界面就开始出状况该居中的不居中、该撑满的撑不满、滑动起来掉帧。所以我想先强调一个已经被说烂但永远值得再说的观点布局XML决定的不只是长什么样它直接决定View树的层级深度和测量次数这两项和渲染性能强相关。1.2 为什么弄懂布局类型比背属性更重要同一个界面有人用三层嵌套LinearLayout做出来有人用一层ConstraintLayout做出来视觉上看起来一样但性能差异在复杂页面上会被放大。View树的层级每多一层measure、layout、draw三个阶段的工作量都会成倍增加尤其在列表滚动场景里一个Item的嵌套层级过多滚起来就是肉眼可见的掉帧。四种常用布局类型各自的定位不同LinearLayout擅长有序排列但最怕无序嵌套RelativeLayout擅长锚点定位但约束关系一旦复杂就难以维护FrameLayout适合层级叠加做占位、浮层很顺手却不适合做复杂排版ConstraintLayout几乎是前面三者的集大成者用扁平结构表达复杂关系是目前主流项目的首选。搞清楚这些你才谈得上有选型意识。遇到一个界面需求不是条件反射式地拖一个LinearLayout开始堆而是先想清楚这个界面有几种排列关系需不需要叠层控件之间的相对位置会不会变化然后再决定用哪个布局、怎么组织层级。接下来我按顺序逐一把四种常用类型讲透。2. 线性布局 LinearLayout最直观的排列方案2.1 核心机制orientation决定一切LinearLayout的定位简单粗暴让子View排成一行或一列。它的核心变量是orientation属性horizontal就是水平排列vertical就是垂直排列。它的排列规则是顺序摆放、互不重叠。每一个子View都会被放到前一个子View的右侧水平或下方垂直并且不会自动换行子View的总尺寸超出容器宽高时就会被截断或者溢出。这一点非常重要很多人在做横向排列的内容时因为没有限制子View宽度导致内容直接跑出屏幕。LinearLayout的理想使用场景是子View数量固定、排列方向单一、顺序感强的界面。比如一个纵向表单里的每一行左侧标签、右侧输入框又比如底部工具栏里的三个按钮水平排开。这些场景用LinearLayout写起来直观读起来也顺畅。2.2 权重weight的正确打开方式LinearLayout最容易被错用也最需要讲清楚的一个属性就是layout_weight。它的作用本质是按比例分配剩余空间。我举个经典例子。一个水平LinearLayout里有三个按钮宽度都设为0dp权重分别设为1、2、1。系统在测量时父容器宽度先减去三个子View本来要占的宽度这里都是0剩下就是全部宽度再按1:2:1分给三个按钮。最终效果就是三个按钮宽度呈1:2:1的比例。注意到关键细节了吗子View的layout_width一定要设成0dp垂直方向同理设0dp高度权重才能正确工作。很多人随手写了wrap_content结果发现权重比例完全不对就是这个原因。因为系统在计算剩余空间时子View本身占据的测量宽度先被减掉了wrap_content的内容宽度也被包含在内剩余空间再按权重分配时比例自然就乱了。还有一点LinearLayout自身有一个weightSum属性可以手动指定权重总和。比如你希望三个子View按1:1:1分配但又不确定计算出来的剩余空间是否正确可以直接给LinearLayout设android:weightSum3这样系统会以weightSum作为权重分配的分母。这个属性在很多适配场景下挺好用能帮助你精确控制比例但别滥用否则代码可读性会下降。2.3 实操一个表单页面的线性布局写法我直接给一个实际项目里常见的例子一个姓名 输入框的行布局。LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:gravitycenter_vertical android:paddingHorizontal16dp TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text姓名 android:textSize14sp / EditText android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:hint请输入姓名 android:inputTypetext / /LinearLayout这里最值得注意的就是EditText的layout_width0dp配合layout_weight1。左边姓名文字占据固定宽度右边的输入框吃掉剩余所有空间形成一个合理的表单行。如果你不写0dp而写match_parent那么EditText会先试图占满整行左边TextView会被挤没或者错位非常典型。再提醒一个坑父容器orientationhorizontal时子View的layout_weight针对的是水平方向的剩余空间反过来orientationvertical时权重针对的是垂直方向。不要搞混。如果你在一个垂直LinearLayout里给子View设置layout_width0dp和权重想用来分配宽度那是完全不生效的因为宽度分配逻辑跟垂直排列没有关系。3. 相对布局 RelativeLayout用参照物解决复杂排布3.1 对齐与锚点相对谁、怎么相对RelativeLayout看名字就知道它的排列逻辑是参照。你可以让一个View相对于父容器对齐比如alignParentToptrue就是贴住父容器顶部也可以让一个View相对于另一个兄弟View定位比如layout_belowid/title就是把当前View放到id为title的View下方。这个设计让界面布局从按顺序排变成了按关系摆。在处理一些非线性的、元素之间互有参照关系的界面时RelativeLayout比LinearLayout更简洁。比如右上角有个关闭按钮输入框下方紧跟一个提示文字这种位置描述用参照关系来表达非常自然。它常用的属性分两类相对父容器layout_alignParentTop、layout_alignParentBottom、layout_alignParentStart、layout_alignParentEnd、layout_centerInParent、layout_centerHorizontal、layout_centerVertical。相对兄弟Viewlayout_toEndOf、layout_toStartOf、layout_below、layout_above、layout_alignTop、layout_alignBaseline。对齐类属性描述的是两个View某条边对齐而位置类属性描述的是我在你的哪一侧。理解这个区别才能组合出正确的约束。3.2 什么时候该用相对布局RelativeLayout有一个明显的弱点它需要两次measure。第一次是收集所有子View的尺寸和位置依赖第二次才是真正确定布局。所以它本身的性能开销比LinearLayout略高。但在层级扁平化方面的收益很多时候能盖过这个开销。我比较推荐的使用场景是界面里控件数量不多但彼此位置关系复杂比如一个卡片右上角带删除角标、底部带操作按钮需要做重叠效果但又不想引入FrameLayout比如一个背景层上盖一个信息层界面结构是某几个控件围着某个关键锚点布局用RelativeLayout可以避免多层嵌套。反过来如果子View数量很多、排列整齐、像一个列表就别硬用RelativeLayout。那反而会让XML又臭又长约束关系多到自己也看不懂。3.3 实操详情页信息区的锚点排布举个常见的个人中心页签名区写法。需求是这样左边一个头像头像右上角一个VIP角标头像右侧显示昵称昵称下方显示个性签名。RelativeLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:padding16dp ImageView android:idid/avatar android:layout_width56dp android:layout_height56dp android:layout_centerVerticaltrue android:srcdrawable/bg_avatar / ImageView android:idid/vipBadge android:layout_width16dp android:layout_height16dp android:layout_alignEndid/avatar android:layout_alignTopid/avatar android:layout_marginEnd-4dp android:layout_marginTop-4dp android:srcdrawable/bg_vip / TextView android:idid/nickname android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginStart12dp android:layout_toEndOfid/avatar android:layout_alignTopid/avatar android:text产品经理老王 android:textSize16sp android:textStylebold / TextView android:idid/signature android:layout_widthwrap_content android:layout_heightwrap_content android:layout_belowid/nickname android:layout_marginStart12dp android:layout_toEndOfid/avatar android:layout_marginTop4dp android:text一个不加班的产品经理是不完整的 android:textColor#999999 android:textSize13sp / /RelativeLayout这个例子充分体现了RelativeLayout的锚点思维VIP角标分别对齐头像的右边和上边昵称和签名都相对头像的右侧定位而签名又在昵称下方。整体只用一层RelativeLayout就完成了原来的LinearLayout嵌套方案三层的效果。这里有一个负margin的用法值得注意角标如果想压在头像边缘外侧可以用layout_marginEnd和layout_marginTop给负数偏移。实测下来这种方式在大多数屏幕上都稳定但如果角标也需要跟随头像做不同屏幕适配还是要记得用dp而不是px避免不同密度设备上的缩放问题。4. 帧布局 FrameLayout层级叠加的轻量容器4.1 叠加渲染后写在上层的规则FrameLayout的设计理念是所有子View默认都从左上角开始放置后添加的子View会覆盖在先添加的子View之上。它本身不提供排列方向的语义只提供层叠的语义。如果非要用一句话说清楚FrameLayout就像PS里的图层后面的图层盖住前面的图层。因为默认所有子View都从左上角开始所以单个子View想让位置变化得靠layout_gravity来控制。layout_gravity可以取top、bottom、left、right、center等组合值。注意这里的gravity是相对FrameLayout的边和中心点来作用而不是像LinearLayout那样沿主轴/padding顺序摆放。4.2 帧布局的三类典型场景FrameLayout在实际项目里最常见的用途有三个我逐个说下。第一个是最经典的角色作为Fragment的容器。Activity里定义一个FrameLayout然后FragmentManager把Fragment提交进去。FrameLayout刚好提供了一个空白画布Fragment的内容由各自的布局决定互不干扰。这里用FrameLayout并不是因为它的层叠能力需要被用到而是因为它足够轻量、不会自己添加额外排列逻辑非常适合当一个纯占位容器。第二个场景是做占位覆盖层。比如页面加载数据前显示一个Loading圆圈数据加载后隐藏掉或者页面底部弹出一个半透明提示条。这种临时冒出来的层级直接用FrameLayout作为根布局里面既有正式内容也有一个visibilitygone的Loading层。需要显示时setVisibility(View.VISIBLE)就行不用动态addView也不用为了一个遮罩层再去创建独立布局。第三个场景是做角标或红点。比如商品图上盖一个折扣标签头像右上角盖一个红点。做法就是外层用FrameLayout里面先放主体控件再放一个角标控件并通过layout_gravity设置角标的位置。因为后添加的会覆盖先添加的所以角标天然显示在主体控件上面。4.3 实操loading占位与角标效果给一个带Loading占位的通用结构FrameLayout android:layout_widthmatch_parent android:layout_heightmatch_parent TextView android:idid/contentText android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_gravitycenter android:text这里是正式内容 android:textSize16sp android:gravitycenter / ProgressBar android:idid/loadingBar android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter android:visibilitygone / /FrameLayout加载数据的时候把loadingBar设为VISIBLE加载完设为GONE内容TextView始终在底下因为ProgressBar是后添加的所以天然在上一层。这个写法比手动addView要省事得多也不会因为频繁add/remove带来不必要的View重绘。再补一个角标效果。商品图上盖一个包邮标签FrameLayout android:layout_width120dp android:layout_height120dp ImageView android:layout_widthmatch_parent android:layout_heightmatch_parent android:scaleTypecenterCrop android:srcdrawable/bg_product / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitytop|start android:background#CCFF0000 android:paddingHorizontal6dp android:paddingVertical2dp android:text包邮 android:textColor#FFFFFF android:textSize10sp / /FrameLayout这里唯一需要留意的点是FrameLayout的宽高如果固定里面的子View布局宽度用match_parent是没问题的但如果FrameLayout本身是wrap_content里面的子View尽量少用match_parent否则很可能出现测量结果和预期不一致的问题。这种wrap_content套match_parent导致宽度异常的组合是我见过最多的一种FrameLayout踩坑现场。5. 约束布局 ConstraintLayout现代开发的默认答案5.1 约束体系从相对到更灵活的关系表达ConstraintLayout可以说是Google为了解决复杂布局的层级膨胀问题而推出的终极武器。它跟RelativeLayout有相似之处都是通过控件之间的相对关系来定位但ConstraintLayout把这种关系的表达能力扩展得更强大、更精细。在RelativeLayout里你只能用layout_below、layout_alignTop这类固定属性来描述相对关系在ConstraintLayout里你会用app:layout_constraintTop_toBottomOfid/xxx这样一套约束来声明我的顶部约束到xxx的底部。语义上更清晰而且同一个View可以同时受到来自上下左右不同方向的约束定位的灵活度比RelativeLayout高出一个量级。这个特性带来的直接好处就是以前需要三四个嵌套布局来描述的复杂界面现在一个扁平ConstraintLayout就能搞定。View树层级变浅measure和layout的开销大幅下降列表滚动流畅度自然就上去了。5.2 常用约束属性速查给新手朋友整理一份最常用的约束属性参考建议收藏起来方向约束属性含义水平app:layout_constraintStart_toStartOfparent我的左边对齐父容器左边水平app:layout_constraintStart_toEndOfid/xxx我的左边在xxx的右边水平app:layout_constraintEnd_toEndOfparent我的右边对齐父容器右边水平app:layout_constraintEnd_toStartOfid/xxx我的右边在xxx的左边垂直app:layout_constraintTop_toTopOfparent我的顶部对齐父容器顶部垂直app:layout_constraintTop_toBottomOfid/xxx我的顶部在xxx的下方垂直app:layout_constraintBottom_toBottomOfparent我的底部对齐父容器底部垂直app:layout_constraintBottom_toTopOfid/xxx我的底部在xxx的上方居中app:layout_constraintStart_toStartOfparent 且 app:layout_constraintEnd_toEndOfparent水平居中居中app:layout_constraintTop_toTopOfparent 且 app:layout_constraintBottom_toBottomOfparent垂直居中居中上面四种约束同时设置完全居中除了这些基础约束还有几个高频使用的辅助工具Guideline一条不显示的辅助线可以按百分比定位用来做不同屏幕的适配分布Barrier一个虚拟参考线可以自动选择一组View中最靠边的那个作为边界适合处理文案长度不确定另一个View要跟在最长文案后面的场景Chain多个View之间首尾约束形成的链式结构可以控制一组控件在空间里的分布方式比如均匀分布、权重比例分布。ConstraintLayout还有一个特别实用的能力bias偏移。在水平方向同时设置Start和End约束后可以再加一个app:layout_constraintHorizontal_bias0.3让控件居中偏左30%的位置。这个能力在RelativeLayout里很难实现但ConstraintLayout直接用属性搞定。5.3 实操一个卡片布局用约束方式完成我拿一个典型的个人名片卡片来做例子展示ConstraintLayout如何统一处理各种布局关系。需求卡片左侧头像右侧两行文字姓名、一句话简介卡片右下角一个操作按钮。androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:padding16dp ImageView android:idid/avatar android:layout_width48dp android:layout_height48dp android:layout_marginEnd12dp app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent android:srcdrawable/bg_avatar / TextView android:idid/name android:layout_width0dp android:layout_heightwrap_content app:layout_constraintStart_toEndOfid/avatar app:layout_constraintEnd_toStartOfid/actionBtn app:layout_constraintTop_toTopOfid/avatar android:text张三 android:textSize16sp android:textStylebold / TextView android:idid/desc android:layout_width0dp android:layout_heightwrap_content app:layout_constraintStart_toEndOfid/avatar app:layout_constraintEnd_toStartOfid/actionBtn app:layout_constraintTop_toBottomOfid/name android:layout_marginTop4dp android:textAndroid开发工程师 / 十一行代码战士 android:textSize13sp android:textColor#999999 / Button android:idid/actionBtn android:layout_widthwrap_content android:layout_heightwrap_content app:layout_constraintEnd_toEndOfparent app:layout_constraintBottom_toBottomOfparent android:text关注 / /androidx.constraintlayout.widget.ConstraintLayout注意几个要点name文字的End约束到actionBtn的Start位置这样当按钮关注变成已关注宽度变化时name的宽度会自动收缩不会遮挡按钮desc文字同样约束End到actionBtn保证长简介不会跑到按钮底下去头像用Bottom和Top同时约束到父容器再配合上面的高度可以实现垂直居中效果不需要额外包裹一层。这种写法等于用相对约束关系来替代固定间距排列界面在不同屏幕宽度下的自适应能力会强很多。约束关系写好了很多尺寸变化都交给系统自动处理而不是在代码里做一遍又一遍的宽高计算。5.4 性能优势与迁移建议从性能角度看ConstraintLayout在measure阶段的策略比RelativeLayout更聪明它可能会分批处理没有依赖关系的View从而减少整体测量时间。再加上扁平化的层级结构复杂页面上的优势相当明显。如果你现在有一个老项目里面还是大量RelativeLayout和LinearLayout嵌套我建议你用ConstraintLayout做渐进式迁移。不用一次性全部重写可以从列表Item和复用频率高的卡片布局开始。这类布局在RecyclerView里会被反复measure和draw优化一个Item的嵌套层级对列表滑动流畅度的提升是最直接的。不过迁移的时候有一个口诀要记住每个View至少要有水平和垂直各一个约束。只写Start不写End控件就悬浮不定只写Top不写Bottom控件就不知道自己在垂直方向上的位置。这是一个新手最容易犯的错写出来的布局在预览和真机上飘忽不定。6. 选型建议、常见问题与性能优化6.1 四种布局怎么选一张表解决纠结总结一下我平时做布局选型时的判断逻辑直接套用就行场景特征推荐布局理由单方向列表、顺序清楚、结构固定LinearLayout语义清晰、代码最少、性能可控少数控件互有锚点关系且层级不多RelativeLayout锚点定位方便避免过度嵌套纯占位容器、Fragment容器、叠加浮层FrameLayout最轻量天生支持层叠复杂页面、控件多、需要多端适配ConstraintLayout层级扁平约束灵活性能最优如果你第一个念头是界面挺复杂不知道用啥那直接选ConstraintLayout基本不会错。它现在已经成为几乎所有新项目的默认布局方案。反过来如果仅仅是一场文字左、输入框右的线性结构硬上ConstraintLayout反而会让XML变得冗长这时候一个LinearLayout更合适。选型的原则不是谁更强而是谁更合适、谁更容易维护。6.2 布局嵌套与性能为什么层级越浅越好布局嵌套层级和渲染性能的关系用一句话概括每多一层嵌套系统在measure阶段就可能多做一次测量在layout阶段多做一次位置计算在draw阶段多做一次绘制裁剪。尤其在高度复杂的页面里嵌套带来的开销是累加的。我见过最夸张的一个老项目一个列表Item里叠了大概七层LinearLayout。视觉上没毛病但滑动的时候帧率经常掉到30帧以下。问题定位到布局后发现光measure就执行了几千次。把这个Item用一层ConstraintLayout重写之后帧率直接恢复到接近满帧。这个真实经历让我后来养成了一个习惯写布局前先画层级草图层级超过三级就要开始警惕想办法拍平。拍平层级有三个常用手段用ConstraintLayout替代外层居中 内层排列 内层再排列的多层结构用Merge标签减少无效的父布局层级特别是在include场景下能用visibility控制的覆盖层就不要动态add/remove View。6.3 排查技巧与自查清单最后分享几个平时排查布局问题会用到的工具和思路。布局层级分析可以用Android Studio自带的Layout Inspector。打开正在运行的App界面选到对应页面就能以3D形式查看View树的层级关系一眼看出哪些层级是多余可优化的。搭配Layout Inspector的还有Show Draw Bars功能会高亮显示每个View的绘制区域方便定位哪里在重复绘制。自查清单方面我每次提交布局代码前会过一遍下面这些问题一个界面的View树是否超过3层超了能不能拍平有没有在ListView或RecyclerView的Item里用过weightweight在滑动场景里带来的measure开销比普通布局大得多有没有在窄容器里使用wrap_content套match_parent这种组合的测量结果经常让人意外所有ConstraintLayout的View是否都有水平和垂直两组约束缺少约束的布局在不同屏幕上的表现会漂移如果有重叠效果用FrameLayout还是ConstraintLayout如果只是简单的角标盖图哪个更简洁用哪个不用硬上更复杂的方案表格型的界面多行多列用TableLayout还是GridLayout/CustomView如果列数固定且行数动态优先考虑GridLayout语义更贴近。这些问题看起来琐碎但对布局的健壮性和性能影响非常大。养成自查习惯之后你会发现很多莫名其妙的渲染问题其实在写代码的时候就能避免。我个人做布局优化这几年最深的体会是布局资源没有万金油也没有一套规则能适配所有需求。真正靠谱的判断方式是先理解每种布局的测量和排列机制再结合具体场景做取舍。LinearLayout的直观、RelativeLayout的锚点、FrameLayout的层叠、ConstraintLayout的灵活各有各的用途没有谁完全优于谁。你需要在动手之前想清楚界面结构和元素之间的关系然后选出最省层级、最易维护的方案。希望这篇内容能帮你少走一些我当年走过的弯路把布局这块基本功打得再扎实一点。
返回列表