刚接触Android布局时几乎所有人学的第一个容器就是线性布局LinearLayout。它把子View按水平或垂直方向排成一条直线看起来没有任何门槛但实际写起来两个按钮宽度为什么不一致layout_gravity设了没反应嵌套一多就卡这类问题就算写了三五年代码的人也会踩。这篇文章我打算把线性布局从设计逻辑、属性原理、权重计算到真实页面搭建、排错方法完整过一遍既适合刚入门的新手也适合那些被gravity和weight折磨到怀疑人生的开发者。1. 为什么是线性布局设计逻辑与适用边界1.1 它解决的到底是什么问题线性布局解决的核心问题只有一句话让一组子View按照先后顺序对齐排列。垂直排列就是从上往下一个个排水平排列就是从左往右一列列排。这里的对齐不是说随便摆而是系统自动按顺序计算每个子View的位置开发者只需要告诉它方向、尺寸、权重剩下的坐标计算全部交给框架。这个逻辑和生活中排队很像大家按顺序站成一列每个人的位置由前面的人决定。对应到Android里父布局记录前一个View的结束位置把它当作下一个View的起始位置中间再留出margin、padding等空隙。LinearLayout默认不会重叠子View也不会自由摆放这也是它名字里线性二字的由来。很多人觉得线性布局太简单不值得深究但从实际项目看它依然是出现频率最高的基础布局之一。原因很现实很多界面天然就是一行一行或者一列一列排下来的用LinearLayout写起来最短最直白。尤其在不同屏幕尺寸下配合权重能做到漂亮的自适应这是其他布局很难替代的。1.2 为什么它依然值得认真学现在ConstraintLayout被官方主推很多人直接从约束布局入门导致对LinearLayout的底层逻辑一知半解。这里我想说一个观点LinearLayout是理解Android测量与布局机制的最佳入口没有之一。原因在于LinearLayout的行为足够简单可预期非常适合用来学习measure、layout这两个核心阶段。你设置一个wrap_content系统怎么量设置match_parent系统怎么量设置weight后又发生了什么变化——这些在LinearLayout里很容易通过实验观察。把这一套搞明白后面再看ConstraintLayout、FrameLayout很多概念都是相通的。从实际开发效率角度看线性布局也不是能被完全替代的。登录页面的纵向排列、底部按钮的一行平分、列表项里左侧图标右侧文字的经典结构用LinearLayout写出来代码量少可读性高配合layout_weight做比例分配尤其方便。与其盲目追求少嵌套而把所有布局都强行改成约束布局不如先确认线性布局在这个场景下是不是更直接的答案。1.3 哪些场景应该绕开线性布局不是万能钥匙。一旦界面需要自由定位、层叠覆盖、复杂网格时硬用LinearLayout就会堆出一大串嵌套。我见过最夸张的案例是一个简单的四宫格开发者用三个横向LinearLayout嵌在一个垂直LinearLayout里每行再套两个子LinearLayout代码写了上百行测量时系统层层计算打开页面有明显卡顿。这种场景显然应该用GridLayout或者ConstraintLayout。不过嵌套本身也不是洪水猛兽两三层的线性布局性能完全没问题真正要警惕的是超过四层的深度嵌套。因为每次measure都是从父到子递归调用层级越深调用次数越多遇到复杂布局很容易掉帧。我自己的选择标准很简单内容排列符合线性顺序就用LinearLayout需要按行列网格就考虑GridLayout需要自由约束、百分比定位、复杂对齐就上ConstraintLayout。每种布局都有它的生态位没必要神化某一个。2. 核心属性全拆解从orientation到weightSum2.1 orientation与宽高尺寸LinearLayout最根上的属性是android:orientation、android:layout_width和android:layout_height。orientation只有两个值horizontal和vertical默认值是horizontal。这个默认值是个经典坑很多人写LinearLayout时忘了声明orientation结果发现子View全部横着排跟直觉里的应该竖着排完全相反。尺寸方面match_parent表示占满父容器剩余空间wrap_content表示包住内容也可以写具体dp值。线性布局有一个很重要的特性在主轴方向也就是orientation指定的方向上子View的layout_width或layout_height会直接影响后面的权重计算。很多权重不生效的假象根源就是子View在主轴方向上用了wrap_content导致初始尺寸不为零分掉了一部分空间。副轴方向通常简单一些。垂直布局时副轴是水平方向子View宽度可以match_parent也可以wrap_content再配合layout_gravity控制对齐。水平布局时副轴是垂直方向子View高度一般wrap_content再用gravity控制垂直居中。还要区分两个尺寸概念margin和padding。margin是当前View与外部兄弟节点或父容器之间的间距padding是当前View内部内容与自身边框之间的间距。两者在测量时的计算方式不同margin会占用父容器的剩余空间padding则在布局内部被消耗掉。2.2 gravity与layout_gravity的混淆点这两个属性是新手重灾区几乎每周都会有人因为这个写错布局。android:gravity是加在LinearLayout父容器上的控制的是所有子View内容在容器内的对齐方式android:layout_gravity是加在子View上的控制的是这个子View在父容器内的对齐方式。名字长得像作用对象完全不同。举一个具体对比在垂直LinearLayout里放一个TextViewTextView宽度是wrap_content。如果给LinearLayout设置gravitycenter_horizontal那么TextView会水平居中如果给LinearLayout设置gravitybottom容器里的TextView会被压在底部。但如果给TextView本身设置layout_gravitycenter_horizontal同样会居中——看起来结果一样实际原理不同。一个是父容器统一指挥所有子View一个是子View单独表达位置诉求。还有一个经常被忽略的点在LinearLayout的主轴方向上layout_gravity往往无效。垂直布局时子View垂直方向的位置已经被排列顺序决定了你写layout_gravitycenter_vertical基本没用水平布局时layout_gravitycenter_horizontal也不起作用。想要在主轴方向偏移只能用布局嵌套或者margin来做。2.3 weight与weightSum的定位权重是LinearLayout区别于其他布局的最核心能力它的作用可以概括为一句话让子View按比例瓜分剩余空间。这里的关键词是剩余而不是整体。后面我会专门讲计算公式现在先建立一个整体印象。weightSum表示父容器声明出来的总权重值。默认情况下系统把所有子View的weight值相加作为总分母。如果手动指定weightSum分配比例就以这个数字为准。给人的感觉像是人为规定了整个空间分成多少份再让各个子View申领其中的几份。一个典型场景底部两个按钮想平分宽度。可以把两个Button都放在一个水平LinearLayout里宽度都设0dpweight都设为1父容器不声明weightSum系统自动算出总权重为2每个按钮分到一半。如果想做成第一个按钮占四分之一第二个按钮占四分之三就把第一个weight设为1第二个设为3总权重为4。如果容器里还混着没有weight的子View那些View先按照layout_width或layout_height占据原始尺寸剩下的空间才交给设置了weight的View去分。这个机制很容易让人困惑但理解了公式后一切都会变得清晰。2.4 divider与基线对齐divider相关属性常被忽略但很实用。android:divider指定一个Drawable作为分割线android:showDividers控制显示位置取值有none、beginning、middle、end支持用竖线分隔多个值。如果列表项之间需要一条细线不用额外画View直接在LinearLayout里配一个shape Drawable就能搞定。使用divider时有几个细节容易出错。第一showDividers默认是none只设divider不设showDividers什么都不会显示。第二divider的尺寸和方向要匹配水平LinearLayout里的分割线通常是一个宽1dp、高match_parent的矩形垂直LinearLayout里的分割线则相反。第三dividerPadding不是每个方向都生效在水平LinearLayout里设置dividerPadding改的是分割线上下两端的间距而不是左右间距。baselineAligned是另一组关于文字基线的属性。默认值为true表示水平LinearLayout里的子View按文字基线对齐。如果你在水平方向排了一个TextView和一个EditText希望它们的文字底边对齐而不是顶部对齐用这个属性就可以实现。baselineAlignedChildIndex可以指定以第几个子View的基线为基准。但实际项目里基线对齐很容易造成用起来莫名其妙的偏移尤其是混入Button、ImageView这种没有明确文本基线的控件时我会直接关掉baselineAligned改用layout_gravitycenter_vertical保持视觉居中。3. 权重机制深入公式、案例与常见坑3.1 剩余空间分配公式的手算过程要真正搞懂weight必须做一次手算。假设父容器是一个水平LinearLayout总宽度为200dp里面有两个Button。按钮A设置layout_width100dpweight1按钮B设置layout_width50dpweight1。系统第一遍测量时先按照每个子View的layout_width算出原始尺寸A占100dpB占50dp两者合计150dp。父容器总宽200dp剩余空间就是200减150等于50dp。接着开始分配剩余空间总权重是1加1等于2A分到50乘以二分之一等于25dpB也分到25dp。最终A宽125dpB宽75dp。看到这里你应该明白了权重不是在原始宽度上按比例缩放而是在剩余空间上按比例分配。所以两个子View即使weight完全相同如果原始尺寸不同最终宽度也完全不同。这就是为什么很多人写了weight之后发现怎么还不是均分的原因。如果你想让两个子View严格等比正确做法是把它们的主轴尺寸都设为0dp。此时A原始尺寸是0B原始尺寸也是0剩余空间等于父容器总宽200dp总权重为2A和B各分到100dp真正实现了五五分。3.2 wrap_content与0dp的经典纠葛很多人在使用weight时保留layout_widthwrap_content期望达到按比例分配的效果结果发现内容更长的那个View明显更宽。这仍然是公式在起作用。我们再来算一个例子父容器宽300dp两个TextView左边内容实际宽80dp右边内容实际宽20dp两者weight都为1。第一遍测量后左边原始尺寸80右边原始尺寸20剩余空间200。按权重分配后两边各得到100。于是左边最终宽度是180右边最终宽度是120。表面上看两个View的weight都是1但宽度比是3比2而不是1比1。内容越长原始尺寸越大差距越明显。解决套路非常简单只要你想在主轴方向按权重精确分配就把主轴方向上的layout_width或layout_height设置为0dp。0dp消除了原始尺寸这个变量让剩余空间成为唯一决定性因素。反过来如果你希望先让View按照内容自适应、再用权重处理多余空间那就要有意保留wrap_content并利用上面的公式做混合布局。3.3 weightSum的使用边界与两个大坑weightSum的常见用法是规定剩余空间的分母。比如有三个按钮我希望B占剩余空间的一半A和C只按内容显示。可以给父容器设android:weightSum2给B设置android:layout_weight1A和C不设weight。这样剩余空间先减去A和C的原始宽度剩余值乘以二分之一归B。这个写法很适合做自适应间距比写死dp值优雅得多。但两个坑必须记住。第一个如果所有子View的weight之和大于weightSum系统不会报错表现是子View们按各自权重抢占超出分母的份额最终比例可能完全失控。第二个如果weightSum设得很大而某个子View的weight也很大的时候这个View可能会把剩余空间全部吃掉其他子View被压缩到最小甚至消失。排查这类问题最直接的办法是把weightSum先删掉观察默认情况下的比例确认无误后再把总和带进去。如果删掉后一切正常加回来就出问题那一定是weightSum与子View总权重不匹配。3.4 测量过程中到底发生了什么虽然普通开发者不需要精通View的measure源码但知道基础流程能帮你少踩很多坑。LinearLayout在onMeasure阶段会做两次测量。第一次测量先按照子View的layout_width或layout_height、margin等计算出每个子View的原始尺寸得到剩余空间如果检测到有子View设置了weight并且剩余空间不为0就会触发第二次测量把新计算出来的尺寸赋给对应子View。这正是weight在wrap_content下不够精确的根本原因第二次测量不是从零开始而是在第一次wrap_content结果的基础上叠加剩余空间的分配。也就是说原本内容决定尺寸的结果被保留了一部分权重只能分配到剩余的那部分。还有一个隐蔽问题如果你给某个子View设置weight0它不参与分配但如果同时把它主轴方向上的尺寸也写成0dp它的原始尺寸就是0最终可能完全不显示。遇到这种情况要么给一个具体宽度要么把尺寸改回wrap_content。4. 从零搭建一个真实界面完整实操过程4.1 目标场景描述为了让前面这些概念落到实处我模拟一个非常典型的界面登录表单页。页面从上到下依次是标题、手机号输入框、密码输入框、性别选项行、底部按钮行、提示文本。这个页面只用LinearLayout就能完成同时能练到orientation、gravity、layout_gravity、weight、margin、divider等几乎全部基础属性。4.2 第一版搭建垂直骨架我先写根节点LinearLayout方向设成vertical让所有内容按从上到下的顺序排列。为了让界面不顶到屏幕边缘给根布局设置16dp的padding。输入框之间用marginTop来分隔而不是加入一堆空的View占位。空View占位是新手很常见的坏习惯它会让布局层级变多而且维护起来很痛苦一个margin就能解决的事完全没必要引入新节点。下面是第一版的XML骨架LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding16dp TextView android:idid/tv_title android:layout_widthwrap_content android:layout_heightwrap_content android:text欢迎登录 android:textSize24sp / EditText android:idid/et_phone android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入手机号 android:inputTypephone android:layout_marginTop16dp / EditText android:idid/et_password android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入密码 android:inputTypetextPassword android:layout_marginTop16dp / /LinearLayout这里有两个细节值得说明。第一个TextView宽度用wrap_content因为标题文字撑开就够了不需要占满整行这样点击区域只在文字周围不会误触整行。第二个EditText宽度用match_parent因为输入框希望尽量宽方便用户点击和输入。4.3 第二版加入性别选项行接下来加一行性别选项用水平LinearLayout容纳两个RadioButton。父容器宽度设wrap_content这样只是包住内容不占满整行。两个RadioButton之间用marginStart隔开。LinearLayout android:layout_widthwrap_content android:layout_heightwrap_content android:orientationhorizontal android:layout_marginTop16dp RadioButton android:idid/rb_male android:layout_widthwrap_content android:layout_heightwrap_content android:text男 / RadioButton android:idid/rb_female android:layout_widthwrap_content android:layout_heightwrap_content android:text女 android:layout_marginStart16dp / /LinearLayout我在这里用了marginStart而不是marginLeft这是Android官方推荐的做法目的是兼容从右往左排列的语言区域。中文环境下两者效果一致但从代码规范角度新项目最好统一使用Start和End。RadioButton默认自带一定的内边距所以不需要额外加太多间距。4.4 第三版用权重做底部按钮行经典的登录页底部通常会放登录和注册两个按钮各占一半宽度。如果直接把两个Button宽度写成match_parent第一个Button会占满一行第二个只能另起一行。正确写法是把它们放进一个水平LinearLayout里宽度match_parent两个Button的宽度都设为0dp再分别设置weight1。LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:layout_marginTop24dp Button android:idid/btn_login android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:text登录 / Button android:idid/btn_register android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:text注册 / /LinearLayout这里特别提醒两个Button之间不要只在一个按钮上添加marginEnd因为这样会导致两侧剩余空间分配时出现微小偏差视觉上两个按钮宽度会差1到2dp。如果真的需要按钮之间的间距正确做法是给两个Button分别设置左右margin并且让它们的横向margin值加起来一致或者用一个固定宽度的Space控件作为间隔。最省心的方案是直接不设间距用背景色或分割线来区分。4.5 第四版加一条分割线和提示信息在性别选项和按钮行之间如果想加一条水平分割线可以用divider。先定义一个shape Drawable文件divider_h.xmlshape xmlns:androidhttp://schemas.android.com/apk/res/android android:shaperectangle size android:height1dp / solid android:color#DDDDDD / /shape然后在任意LinearLayout中启用android:dividerdrawable/divider_h android:showDividersmiddle android:dividerPadding8dp注意这个divider如果用在水平LinearLayout里高度必须设置为1dp但宽度要足够参考实际需求如果用在一个垂直LinearLayout里shape的宽度要设为match_parent高度保持1dp。大部分人不显示分割线的原因都是忘了写showDividers因为默认值是none。提示信息我放在底部宽度match_parent文字用gravityend靠右显示。这里用gravity而不是layout_gravity是因为TextView已经占满整行需要控制的是文字在控件内部的位置。如果写成layout_gravityend由于控件本身宽度已经等于父容器宽度没有任何剩余空间属性会被忽略。4.6 运行效果与调整实录把上述页面跑起来后最典型的问题是输入法弹出后页面被顶上去底部按钮被软键盘遮挡。这个问题的根因不在LinearLayout而是Activity的软键盘模式。在Manifest里给当前Activity设置android:windowSoftInputModeadjustResize并且把根布局替换成ScrollView包裹就能在多数情况下避免遮挡。这里要特别说明ScrollView本身不是布局容器思维它只允许一个直接子View。通常做法是ScrollView下面直接挂一个垂直LinearLayout作为唯一子View再在这个LinearLayout里放内容。同时ScrollView需要设置fillViewporttrue否则当内容高度不足时内部LinearLayout不会自动撑满全屏背景色或底部内容的位置会和预期不符。另外在调试过程中我发现标题TextView一开始用了match_parent加上gravitycenter_horizontal等价于文字居中但整个标题区域会占据一整行宽度导致点击标题时整个屏幕宽度范围都是响应区域。后来改成wrap_content加layout_gravitycenter_horizontal让控件只包住文字本身点击区域更合理。5. 常见问题与排查技巧实录5.1 layout_gravity设了没反应这个问题的出现频率非常高。报错形式通常是在垂直LinearLayout里给子View设了layout_gravitycenter_vertical但位置纹丝不动。首先要理解LinearLayout的layout_gravity只在副轴方向生效。垂直布局的副轴是水平方向所以center_horizontal、start、end这些有效而center_vertical属于主轴方向基本无效。水平布局则相反layout_gravitycenter_horizontal无效center_vertical才有效。除了方向问题还有两个容易踩的原因。第一个是控件尺寸已经占满了父容器的剩余空间比如宽度match_parent没有剩余空间自然无法对齐。第二个是属性写错了位置layout_gravity是子View的属性而不是父容器的属性很多人写在LinearLayout节点上当然不会对子View产生任何效果。检查时可以先在调试阶段加上不同背景色确认剩余空间是否存在再判断方向问题。5.2 权重没有按预期比例分布权重问题九成出在layout_width或layout_height与weight的组合上。前面反复强调过只有0dp加weight才能实现纯粹的按比例分配。如果发现weight为2的按钮只比weight为1的按钮多了一丁点宽度几乎可以断定那个按钮的宽度是wrap_content内容大小把权重效果稀释了。另一个隐蔽坑是父容器在主轴方向上的尺寸是wrap_content。如果你给垂直LinearLayout高度设为wrap_content那么子View们拿到的剩余空间天然就是0因为父容器的高度等于所有子View高度之和。此时子View就算写了weight也不会生效。把父容器改成match_parent或固定高度效果立刻出现。weightSum不匹配也会引发诡异现象。当子View的总权重大于weightSum时比例会失真当总权重小于weightSum时剩余空间分不完留出一段空白。排查思路是去掉weightSum用默认总权重跑一遍看比例是否正常。5.3 嵌套过多导致卡顿或宽度无限撑大LinearLayout层层嵌套带来的问题有两个一是性能二是布局计算的不确定性。在measure阶段每一层都要递归调用子View的测量方法层级越深调用次数越多。如果页面上有列表列表项里再嵌套五层LinearLayout滚动时卡顿会非常明显。优化方式不只是改成ConstraintLayout这一条路。很多嵌套可以用单个LinearLayout加weight解决。比如左侧图标、中间文字、右侧箭头这类经典列表项一个水平LinearLayout就能搞定没必要再包一层相对布局。真正复杂到自由定位、百分比约束的时候再考虑ConstraintLayout也不迟。宽度无限撑大这个问题也常见于水平LinearLayout某个子View内容非常长且子View宽度是wrap_content系统会优先满足它的测量需求导致这个子View把父容器宽度不断撑大直到超出屏幕。解决办法是给这个子View设置layout_width0dpweight限制它在父容器宽度范围内分配空间或者使用android:maxWidth限制最大宽度。5.4 divider不显示或显示位置不对如果你配好了divider却什么都没出现检查顺序如下。第一确认showDividers不是none必须设置为beginning、middle、end或者组合。第二确认Drawable的尺寸方向与LinearLayout方向匹配。水平LinearLayout需要宽度很小的分割线垂直LinearLayout需要高度很小的分割线。如果是shape宽度或高度至少要有一个确定值。第三确认Drawable的可见性solid颜色不能是纯透明。如果divider出现了但位置奇怪多半是dividerPadding的方向理解错了。在水平LinearLayout中dividerPadding调整的是分割线上下两端的间距在垂直LinearLayout中调整的是左右两端的间距。很多人在水平布局里想调整左右间距却设置dividerPadding自然没有效果。5.5 基线对齐造成意外偏移baselineAligned默认是true在水平排列的多个纯TextView场景下通常符合直觉。但一旦混入Button、EditText、ImageView基线参考点就很难控制。Button文本默认垂直居中EditText有自己的文字内边距ImageView完全没有基线概念系统最终只能强行找某个子View的基线来对齐于是整个组件组出现肉眼可见的偏移。我比较推荐的替代方案是先用gravitycenter_vertical让组件在副轴方向居中如果必须让两段文字底部对齐就给他们设置相同的高度再用gravitybottom内部对齐。这样比依赖baselineAligned稳定得多。当然理解baselineAligned仍然有必要因为不少第三方控件内部会默认使用它出了问题要知道去android:baselineAlignedfalse关闭。5.6 我说一个一直有效的排查习惯写了很多年布局我慢慢养成了一个固定习惯每写一个LinearLayout之前先在脑子里把主轴方向想清楚然后问自己三个问题。第一子View在主轴方向需不需要按比例分配空间如果需要就坚定地使用0dp加weight。第二副轴方向要对齐到什么位置用父容器的gravity还是子View自己的layout_gravity第三父容器在主轴方向是否有剩余空间如果没有权重写了也白写。这三步想清楚大部分布局异常都能在写代码之前避免。另一个小技巧是调试时给LinearLayout和子View都加上临时背景色用色块直观观察每个控件实际占用的空间边界比盯着属性文件猜快得多。问题修复后再把背景色删掉。这套方法陪伴了我很长时间遇到再复杂的布局问题我都会先回到这一步。
阅读完成 · 觉得有帮助?