Day09了构建布局这个专题终于进入下半场。前面几篇我把Row、Column、Stack、Flex这些基础容器讲得算是比较透了——横向怎么排、纵向怎么排、谁压谁、谁弹性伸缩这套框架搭起来之后很多页面确实能拼出来。但真到了项目里你会发现光靠那四个容器远远不够。一个详情页要按锚点定位一个首页要兼顾横竖屏和折叠屏一个列表要处理上千条数据的懒加载这些场景都指向同一个结论布局的进阶玩法必须跟上。今天的下篇就是把这部分硬骨头啃下来。这篇内容重点放在相对布局RelativeContainer、列表List、网格Grid、栅格GridRow/GridCol还有滚动嵌套的实战处理上。适合的人群也明确一下已经能熟练用Row、Column写静态页面但面对复杂页面还是靠盲目堆容器的开发者准备冲鸿蒙应用开发高级认证、需要系统性理解布局体系的考生以及正在做多设备适配、被列表卡顿和嵌套滚动折磨的项目组成员。跟着代码走一遍再把后面的坑位清单存下来基本能少走两个月的弯路。1. 布局体系回顾与今日内容定位1.1 上篇留下了哪些问题上篇结束时有三位同学私信问了我三个特别典型的问题正好可以当作今天内容的引子。第一位同学说他想用Row和Column做一个“图片在左、文字在右、右上角有个角标”的卡片结果嵌套了五层容器看着头皮发麻。这就是线性布局的天然局限——它只擅长管“一排”或者“一列”一旦元素之间需要“相对于另一个元素定位”线性布局就得靠层层嵌套来硬凑。第二位同学说他的商品列表有一万条数据直接写了个ForEach渲染结果应用启动直接卡了好几秒滚动的时候还掉帧。这其实是列表性能的经典问题ForEach全量创建节点对长列表来说就是灾难。第三位同学说页面在手机上好好的一放到折叠屏和平板上全乱了。他问是不是要每种设备各写一套页面这显然不是正解。这三个问题分别对应了今天要讲的三个核心内容相对定位、懒加载列表、栅格自适应。把它们解决掉“构建布局”这个专题就算真正闭环了。1.2 今日布局清单与选型逻辑为了不迷失在API的海洋里先把今天涉及的布局和它们各自解决的“核心矛盾”列清楚布局容器解决的核心问题典型使用场景RelativeContainer元素之间、元素与容器之间的相对锚定页面头部导航、悬浮角标、右上角操作区List LazyForEach海量单列/多列数据的按需渲染消息流、商品列表、动态信息流Grid多行多列网格支持跨行跨列宫格入口、商品墙、相册GridRow/GridCol多端设备下的栅格自适应平板/折叠屏/手机统一适配Scroll滚动容器处理溢出内容长表单、详情页整体上下滚动选型逻辑其实一句话就能概括先想清楚“元素之间的关系是什么”再选容器。线性关系用Row/Column层级压叠用Stack锚点相对关系用RelativeContainer重复数据用List/Grid跨端等宽划分用GridRow/GridCol。不是哪个布局“高级”就用哪个而是哪个能最准确地表达页面结构就用哪个。2. 相对布局RelativeContainer先弄懂锚点再谈自适应2.1 为什么必须掌握相对布局RelativeContainer在中文里叫相对布局它的设计目标和CSS里的position定位思路很像但写起来更结构化。它允许你给容器里的每个子组件指定一个“锚点”然后让子组件基于锚点来确定自己的位置。这里的锚点可以是父容器也可以是容器里的任意兄弟组件。打个比方。你往照片墙上贴照片先贴好正中间那张全家福然后其他照片都以全家福为基准左边这张贴纸以全家福左下角为原点往左偏10厘米右上角那张以全家福右上角为原点往上偏5厘米。这个“基准”就是锚点。全家福的位置一旦移动其他照片全部跟着动——这就是相对布局最核心的价值位置联动。在页面开发里最典型的就是详情页头部返回按钮要固定在左上角操作菜单固定在右上角标题要水平居中底部信息条要贴着父容器底部。如果用Row去硬排会被各种间距问题折磨用RelativeContainer每个元素各报各的锚点结构一目了然。2.2 alignRules锚点写法与参数说明直接上一段可以在DevEco Studio里跑起来的代码场景是商品详情页头部导航区RelativeContainer() { Text(←) .id(backBtn) .fontSize(24) .width(40) .height(40) .textAlign(TextAlign.Center) .alignRules({ top: { anchor: __container__, align: VerticalAlign.Top }, left: { anchor: __container__, align: HorizontalAlign.Start } }) .margin({ top: 12, left: 12 }) Text(商品详情) .id(title) .fontSize(18) .fontWeight(FontWeight.Medium) .alignRules({ top: { anchor: __container__, align: VerticalAlign.Top }, horizontalCenter: { anchor: __container__, align: HorizontalAlign.Center } }) .margin({ top: 18 }) Text(···) .id(moreBtn) .fontSize(24) .width(40) .height(40) .textAlign(TextAlign.Center) .alignRules({ top: { anchor: __container__, align: VerticalAlign.Top }, right: { anchor: __container__, align: HorizontalAlign.End } }) .margin({ top: 12, right: 12 }) } .width(100%) .height(64)这段代码有三个关键点一定要理解。第一.id()方法必不可少。在RelativeContainer里子组件之间靠id建立引用关系如果某个组件没写id它就无法成为别人的锚点。第二alignRules里每个属性都是一个对象包含anchor和align两个字段。anchor指定锚点是谁__container__是父容器的固定写法align指定对齐方式。比如left: { anchor: __container__, align: HorizontalAlign.Start }意思就是“我的左边缘对齐容器左边缘”。第三RelativeContainer支持top、bottom、left、right、horizontalCenter、verticalCenter六种对齐规则你可以自由组合。当一个组件同时声明了top和bottom且没有显式设置高度时组件会自适应拉伸高度来满足两端对齐。2.3 自适应细节百分比、margin与offset的配合实际开发中相对布局常常要和百分比宽度、margin、offset一起使用。这里有个很重要的优先级关系alignRules决定的是组件“贴在哪条基准线上”margin决定组件相对基准线再偏多少offset则是在最终位置上再做一次像素级微调。举一个场景底部弹层里的确认按钮希望它始终距离屏幕左右各16vp并且垂直居中。可以这样写RelativeContainer() { Button(确认支付) .id(confirmBtn) .width(100%) .height(48) .alignRules({ horizontalCenter: { anchor: __container__, align: HorizontalAlign.Center }, verticalCenter: { anchor: __container__, align: VerticalAlign.Center } }) .margin({ left: 16, right: 16 }) } .width(100%) .height(200)注意一个细节按钮宽度写的是100%但这个百分比是相对于RelativeContainer内容区的。如果父容器宽度是360vp那按钮实际宽度是360减去左右margin后剩余的空间还是360答案是360margin是在宽度确定之后再计算偏移的。如果你想让按钮宽度真的变成“容器宽减去32vp”不能用width(100%)加margin而应该用left和right双锚定.alignRules({ left: { anchor: __container__, align: HorizontalAlign.Start }, right: { anchor: __container__, align: HorizontalAlign.End }, verticalCenter: { anchor: __container__, align: VerticalAlign.Center } }) .margin({ left: 16, right: 16 })这样写按钮没有设置宽度但通过“左锚定右锚定”把宽度撑起来了最终宽度自动变成容器宽度减去32vp。这是相对布局里非常实用的一招能省掉大量计算。2.4 踩坑记录循环依赖与依赖缺失用RelativeContainer最容易踩的坑有两个我都实打实遇到过。第一个是循环依赖。比如组件A的top依赖组件B组件B的top又依赖组件A系统就会报类似“Circular dependency detected”的错误。这种错误编译期不一定报但运行期布局会直接失效表现出来就是组件位置乱飞。排查方法很简单把每个组件的alignRules列出来画一条“我依赖谁”的箭头只要箭头成环一定有循环依赖。解决方法通常是引入父容器作为中间锚点打破依赖环。第二个是依赖缺失。A组件依赖B组件作为锚点但B组件没有设置id或者B组件在渲染时被条件判断隐藏了A组件就会定位失败。特别容易被忽略的是某些情况下锚点组件的宽高为0A组件虽然定位过去了但看起来像消失了。所以写完相对布局我习惯把整个容器的高亮边框打开在预览器里看一眼每个组件的实际占位排查这类隐藏问题效率会高很多。3. 列表与网格高频场景的进阶玩法3.1 List基础结构不只是竖向列表List是日常开发里用得最多的高频组件但很多人对它的理解停留在“竖向滚动列表”。实际上List容器内部一般配合ListItem来组织行但List本身还支持很多布局形态通过listDirection参数设置轴向横向列表也很常见通过lanes参数设置每行列数可以实现多列商品墙的效果通过sticky参数可以吸顶。一个最基础的竖向列表长这样List({ space: 8 }) { ForEach(this.simpleData, (item: string) { ListItem() { Text(item) .width(100%) .height(80) .backgroundColor(#FFFFFF) .borderRadius(8) } }, (item: string) item) } .width(100%) .layoutWeight(1)space是行间距这个参数放在List的构造函数里不要写错位置。ListItem内可以直接放一个组件也可以放一个Row/Column来承载复杂布局。不过这里要强调ForEach适合数据量在几十条以内的简单场景。一旦数据量上来必须换LazyForEach。3.2 LazyForEach懒加载一万条数据不卡顿的关键很多新手写长列表上来就是ForEach等列表卡爆了才来问为什么。原因很简单ForEach会一次性创建并渲染所有子组件一万条数据就是一万个组件节点闪存和渲染压力都扛不住。LazyForEach的数据源不是普通数组而是需要实现IDataSource接口的类。这个接口要求实现totalCount()、getData(index)、registerDataChangeListener()和unregisterDataChangeListener()四个方法。数据量变化时通过DataChangeListener通知列表局部刷新。给一个可以直接用的例子class ProductListDataSource implements IDataSource { private list: ProductItem[] []; private listeners: DataChangeListener[] []; constructor(list: ProductItem[]) { this.list list; } totalCount(): number { return this.list.length; } getData(index: number): ProductItem { return this.list[index]; } registerDataChangeListener(listener: DataChangeListener): void { this.listeners.push(listener); } unregisterDataChangeListener(listener: DataChangeListener): void { const idx this.listeners.indexOf(listener); if (idx -1) { this.listeners.splice(idx, 1); } } addItem(item: ProductItem): void { this.list.push(item); this.listeners.forEach(listener { listener.onDataAdd(this.list.length - 1); }); } }然后在组件里这样使用List({ space: 12 }) { LazyForEach(this.productDataSource, (item: ProductItem) { ListItem() { ProductCard({ product: item }) } }, (item: ProductItem) item.id) } .width(100%) .layoutWeight(1)这里最关键的是第三个参数——键值生成函数。它返回的id必须是每一条数据唯一的标识。如果你返回的键值重复或者每次渲染都返回一个临时对象LazyForEach的复用机制就会失效列表反而可能出问题。我的习惯是直接用数据库主键没有主键时用item.id这种稳定标识。3.3 网格布局Grid跨行跨列不是玄学Grid和List是兄弟组件List擅长单列或简单多列Grid更擅长规整的多行多列。Grid通过columnsTemplate和rowsTemplate定义网格模板字符串里用数字和空格来分配权重。做一个三列商品墙Grid() { ForEach(this.productList, (item: ProductItem) { GridItem() { ProductCard({ product: item }) } }, (item: ProductItem) item.id) } .columnsTemplate(1fr 1fr 1fr) .columnsGap(8) .rowsGap(8) .width(100%) .layoutWeight(1)columnsTemplate里的1fr 1fr 1fr表示三列等宽。如果想让中间列更宽可以写成1fr 2fr 1fr。这个fr的概念和CSS grid里的fr一样是剩余空间分配单位。GridItem支持跨行跨列这是做复杂宫格布局的杀手锏。给GridItem设置rowStart、rowEnd、columnStart、columnEnd就能让它占据多行或多列GridItem() { BannerEntry() } .columnStart(0) .columnEnd(2) .rowStart(0) .rowEnd(1)注意下标从0开始columnStart(0)到columnEnd(2)表示该元素横向跨越第0、1、2三列。这个功能在自选股面板、运营位广告、个人中心功能区块里非常常用。3.4 List与Grid的性能细节和选型对比关于List和Grid的选型需求本身决定但有几个经验可以分享。如果页面里每一行需要展示的商品数量是固定的用Grid比较直接如果行内信息结构差异大比如有的行是富文本、有的行是视频卡片用List配合不同的ListItem子组件更灵活。性能方面有几个参数务必重视。第一个是cachedCount它控制列表上下两侧预加载的条目数量。实际项目中网络加载图片的列表我会设置为1到3太少容易白屏太多会增加首屏渲染压力。第二个是scrollBar默认是滚动一段时间后才显示如果没有特殊需求建议设为BarState.Auto。第三个是edgeEffect推荐设为EdgeEffect.Spring滚动到边界时会有回弹动画交互体验会自然很多。另外如果Grid里的数据量比较大同样应该使用LazyForEach而不是ForEach。这个错误我见得太多了。4. 栅格布局GridRow/GridCol多设备适配的正解4.1 栅格系统的设计逻辑RelativeContainer解决的是“相对谁”的问题List/Grid解决的是“大量数据怎么排”的问题。但还有一个更宏观的问题没解决手机、折叠屏、平板屏幕宽度差异巨大同一个页面怎么做到自动重排答案是栅格布局GridRow/GridCol。栅格系统在Web端早就被Bootstrap普及了核心思路是把一行分成若干列内容按列数分配宽度不同屏幕宽度下分配策略不同。鸿蒙的GridRow/GridCol也是这个思路但针对多设备做了更细的断点划分。4.2 断点配置与列数分配实操GridRow是栅格容器GridCol是栅格列。GridRow通过columns属性设置不同断点下的列数GridCol通过span属性设置该列在不同断点下占多少列。来看一个经典的“左侧两列导航右侧内容”的页面适配写法GridRow({ columns: { xs: 4, sm: 8, md: 12, lg: 12 }, gutter: { x: 12, y: 12 } }) { GridCol({ span: { xs: 4, sm: 8, md: 3, lg: 2 } }) { NavigationMenu() } GridCol({ span: { xs: 4, sm: 8, md: 9, lg: 10 } }) { ContentArea() } } .width(100%)解释一下这里的断点含义xs对应手机竖屏默认约320vp至359vpsm对应大屏手机约360vp至599vpmd对应平板竖屏约600vp至839vplg对应平板横屏840vp及以上你也可以通过breakpoints自定义断点值。这段代码的含义是在手机竖屏xs断点时整个栅格只有4列左边导航的span是4占满整行右边内容区翻转到下一行在平板竖屏md断点时总共12列导航占3列内容占9列左右并排。这样一套代码两种设备形态都适配了。4.3 多设备适配的实测经验用栅格布局做多设备适配有几个实际经验比文档更有价值。第一个是“不要执着于完美还原”。pad上内容区特别宽时如果还硬把一行拉满阅读体验反而不如“内容区最大宽度限制在某个值两侧留白”。我一般会在内容GridCol内部再套一层constraintSize({ maxWidth: 720 })配合外边距实现居中留白。第二个是“折叠屏一定要考虑展开态”。折叠屏展开前后断点很可能从xs跳到md这时候要保证页面布局是响应式的而不是直接崩掉。开发时可以在DevEco Studio的预览器里直接切换设备形态我建议每个使用栅格的关键页面都在四种断点下各过一遍。第三个是“gutter不等于margin”。gutter是栅格列之间的间距它会把可用空间吃掉。当所有列的span加起来刚好等于总列数时加上gutter后列内容宽度会缩短这是正常的别以为是自己代码写错了。5. 滚动与嵌套布局暗坑排查实录5.1 Scroll与List的选择Scroll是一个纯粹的滚动容器它本身不关心子组件是什么可以滚动任何超出屏幕的内容。在实际开发里Scroll通常配合Column使用用来承载长表单、大段文本、自定义组合内容。而List是“数据驱动”的滚动容器它知道自己在渲染列表数据可以做懒加载、回收复用。所以结论很简单内容是可枚举的同构数据选List内容是异构的、不可枚举的页面区块选Scroll。需要注意一个高频误区有人习惯在Scroll里放一个ColumnColumn里再放一个List。这种嵌套在大部分情况下会引入滚动冲突表现为内容被切掉、滚不动、或者滚起来极其别扭。如果页面确实既需要整体滚动又需要局部列表我的建议是重新评估结构尽量用NestedScroll机制或者把列表数据通通并入外层滚动尽量避免“父子双向滚动”。5.2 嵌套滚动冲突怎么排查鸿蒙提供了onScrollIndex、scrollBy等能力也支持NestedScroll。当你确实需要页面级Scroll嵌套List时可以给List设置nestedScroll参数来控制子滚动器与父滚动器的协作方式。List() { // ... } .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST })scrollForward指手指上滑时滚动的优先级scrollBackward指手指下滑时的优先级。PARENT_FIRST表示父容器优先消费滚动事件SELF_FIRST表示子容器优先。如果你遇到了“列表滚到顶部后页面不肯继续滚动”的问题大概率就是这里的模式设置不对。排查这类问题我有个固定套路出现滚动异常时先把页面里所有Scroll、List拆成单独运行确认每个容器本身滚动正常。然后从最内层开始一层层加回嵌套关系并分别配置nestedScroll。这样一加一测很快就能定位是哪一个环节抢走了滚动事件。5.3 安全区与键盘避让很多布局问题看起来是滚动问题其实是安全区问题。比如页面底部按钮在iPhone式全面屏上被home指示条挡住或者输入框弹键盘后把内容盖住。鸿蒙里处理这个有两个常用能力。第一个是expandSafeArea。它可以扩展组件绘制和安全区边距让页面背景铺满全屏但内部内容避开危险区域需要根据页面UI具体调整。我通常在构建全屏沉浸式页面时使用。第二个是Scroll/List的keyboardAvoidMode。设置成KeyboardAvoidMode.OFFSET后输入法弹出时滚动容器会主动上推确保焦点输入框可见。这个如果没设置弹键盘遮挡输入框的问题几乎必现。经验教训不要在页面顶层写一个onKeyEvent去手动处理键盘顶起又慢又容易出bug。直接给滚动容器设置adykeyboardAvoidMode大部分场景都能覆盖。6. 实战作业用今日布局组合还原一个详情页6.1 页面结构与布局选型把今天讲的所有知识点串起来我来拆一个真实的“商品详情页”。先看页面骨架从上到下依次是顶部导航栏返回按钮、居中标题、右上角更多按钮商品图轮播一张全宽图片区域价格区价格左对齐、销量和收藏右对齐商品信息卡片标题、副标题、规格参数底部操作栏客服、收藏、加入购物车、立即购买布局选型思路如下顶部导航栏和底部操作栏都用RelativeContainer进行锚点定位整页内容使用Scroll包裹因为它的结构是异构区块组合价格区的信息用Row加Blank实现左右推挤规格参数区是一个两列网格用Grid实现。这个结构既覆盖了今天的重点布局也没有刻意为了炫技使用复杂嵌套它是实际项目里非常常见的一种组合。6.2 核心代码实现顶部导航栏直接用之前RelativeContainer那套代码即可这里不重复了。重点看整页结构Scroll() { Column() { // 商品图轮播 Swiper() { Image($r(app.media.goods_1)) .width(100%) .height(320) Image($r(app.media.goods_2)) .width(100%) .height(320) } .width(100%) .height(320) .autoPlay(true) .indicator(true) // 价格与销售信息 RelativeContainer() { Text(¥299) .id(price) .fontSize(28) .fontColor(Color.Red) .alignRules({ left: { anchor: __container__, align: HorizontalAlign.Start }, top: { anchor: __container__, align: VerticalAlign.Top } }) Text(已售1200件) .id(soldCount) .fontSize(14) .alignRules({ right: { anchor: __container__, align: HorizontalAlign.End }, top: { anchor: __container__, align: VerticalAlign.Top } }) Text(收藏 3.2k) .id(favCount) .fontSize(14) .alignRules({ right: { anchor: __container__, align: HorizontalAlign.End }, top: { anchor: __container__, align: VerticalAlign.Top } }) .margin({ right: 80 }) } .width(100%) .height(40) .margin({ top: 12 }) // 商品规格 Grid() { ForEach(this.specList, (spec: string) { GridItem() { Text(spec) .fontSize(14) .padding(8) } }, (spec: string) spec) } .columnsTemplate(1fr 1fr) .columnsGap(12) .rowsGap(12) .margin({ top: 16, left: 16, right: 16 }) } } .width(100%) .layoutWeight(1) .scrollBar(BarState.Auto) .edgeEffect(EdgeEffect.Spring)底部操作栏单独放在整个页面底部不放进Scroll里保证无论内容区怎么滚动操作栏都固定RelativeContainer() { Text(客服) .id(service) .alignRules({ bottom: { anchor: __container__, align: VerticalAlign.Bottom }, left: { anchor: __container__, align: HorizontalAlign.Start } }) Text(收藏) .id(favorite) .alignRules({ bottom: { anchor: __container__, align: VerticalAlign.Bottom }, left: { anchor: service, align: HorizontalAlign.End } }) .margin({ left: 16 }) Button(加入购物车) .id(cartBtn) .alignRules({ bottom: { anchor: __container__, align: VerticalAlign.Bottom }, right: { anchor: __container__, align: HorizontalAlign.End } }) Button(立即购买) .id(buyBtn) .alignRules({ bottom: { anchor: __container__, align: VerticalAlign.Bottom }, right: { anchor: cartBtn, align: HorizontalAlign.Start } }) .margin({ right: 12 }) } .width(100%) .height(64) .backgroundColor(#FFFFFF)这里底部操作栏用RelativeContainer的锚点链实现了“客服在最左收藏在客服右边立即购买在最右加入购物车在立即购买左边”。这种锚点链写法比Row加Blank写出的代码更适合组件的增删改动因为增删一个按钮时只需调整对应锚点关系不用动整行结构。6.3 尺寸适配与自测清单页面写完强烈建议按下面这个清单自测一轮手机竖屏顶部导航、价格区、按钮区是否错位折叠屏展开价格区和规格区有没有拉伸变形平板横屏内容区是否过宽导致阅读困难是否需要套一层最大宽度约束开启大字体文本是否被截断按钮是否换行错位输入法弹出如有输入框是否被键盘遮挡每次改完布局用DevEco Studio预览器切一两种设备形态看一眼比等到真机上再发现问题要省钱得多。7. 常见问题速查与心得整理7.1 高频问题排除表现象大概率原因处理方案RelativeContainer里组件位置乱飞锚点组件循环依赖或依赖的id不存在梳理锚点链打破循环检查id是否设置列表滚动卡顿用了ForEach渲染海量数据换成LazyForEach并实现IDataSourceListItem点击事件不生效事件绑定在了ListItem内部的子组件上且未加上.stopPropagation在ListItem根节点绑定事件必要时阻止冒泡GridItem想跨列却无效只设置了columnStart忘记设置columnEnd同时设置start和end下标从0开始平板横屏页面被拉伸没有使用GridRow/GridCol或栅格列数分配不合理改用栅格布局右侧内容区限制最大宽度键盘弹起遮挡输入框滚动容器没有设置keyboardAvoidMode设置keyboardAvoidMode(KeyboardAvoidMode.OFFSET)底部按钮被安全区遮挡未做安全区适配使用expandSafeArea或布局底部增加安全间距7.2 几条值得记下的心得第一个心得和布局无关但和构建布局强相关写任何复杂页面之前先在纸上或者白板上画一版“结构树”标清楚哪些区块是并列关系、哪些是包含关系、哪些是锚定关系。这个动作做扎实了代码基本不会太乱。我见过太多同事上来就写代码写到一半发现嵌套层级爆炸再推倒重来时间全浪费在返工上。第二个心得频繁修改布局时尽量用alignRules和margin来表达间距而不是在组件里硬编码具体像素值。像素值写起来一时爽后面做多设备适配的时候就是一场灾难。把间距交给对齐规则和margin把弹性交给Blank和layoutWeight把跨端交给GridRow/GridCol整个布局体系会健康很多。第三个心得是关于“布局性能”的可能很多人没在意RelativeContainer内部会对每个子组件做约束计算子组件数量过多时计算开销会上升。一个RelativeContainer里放十几个组件可能还感觉不出来但如果一个页面里有多个超大相对布局性能就会肉眼可见地下降。建议把大块相对布局拆成几个小的RelativeContainer而不是指望一个大容器搞定所有。7.3 动手改造的建议博客看到这里千万别只收藏不实践。我建议你把手头正在做的一个页面拿过来对照今天讲的选型表重新审视一遍现在的嵌套结构能不能用RelativeContainer简化长列表的ForEach能不能升级成LazyForEach多设备适配是不是用了大量if语句判断屏幕宽度这一轮重构做完你对HarmonyOS布局体系的理解会上一个台阶。我在第一家公司的时候带我的师傅说过一句话后来我一直拿它当判断标准好的布局代码应该是别人拿到手之后看结构就能猜到组件之间的从属关系。如果一段布局代码需要画半天草图才能讲明白那它就不是好结构。今天讲的这些容器本质上都是在帮你把“组件之间的关系”表达得更准确。多写、多拆、多试几次这套手感就有了。后面如果时间允许我打算把“布局动画”和“自定义布局”这两个进阶方向也写成实操篇。布局静态结构搭好了下一步就是让它动起来、活起来。各位先把今天的代码跑通有问题可以在下面留言看到都会回。
