news 2026/9/11 5:04:45

鸿蒙布局进阶:相对定位、懒加载列表与栅格多设备适配实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙布局进阶:相对定位、懒加载列表与栅格多设备适配实战

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里每个属性都是一个对象,包含anchoralign两个字段。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?答案是360,margin是在宽度确定之后再计算偏移的。如果你想让按钮宽度真的变成“容器宽减去32vp”,不能用width('100%')加margin,而应该用leftright双锚定:

.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组件就会定位失败。特别容易被忽略的是:某些情况下锚点组件的宽高为0,A组件虽然定位过去了,但看起来像消失了。所以写完相对布局,我习惯把整个容器的高亮边框打开,在预览器里看一眼每个组件的实际占位,排查这类隐藏问题效率会高很多。

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通过columnsTemplaterowsTemplate定义网格模板,字符串里用数字和空格来分配权重。

做一个三列商品墙:

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设置rowStartrowEndcolumnStartcolumnEnd,就能让它占据多行或多列:

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至359vp),sm对应大屏手机(约360vp至599vp),md对应平板竖屏(约600vp至839vp),lg对应平板横屏(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里放一个Column,Column里再放一个List。这种嵌套在大部分情况下会引入滚动冲突,表现为内容被切掉、滚不动、或者滚起来极其别扭。如果页面确实既需要整体滚动,又需要局部列表,我的建议是重新评估结构,尽量用NestedScroll机制或者把列表数据通通并入外层滚动,尽量避免“父子双向滚动”。

5.2 嵌套滚动冲突怎么排查

鸿蒙提供了onScrollIndexscrollBy等能力,也支持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 页面结构与布局选型

把今天讲的所有知识点串起来,我来拆一个真实的“商品详情页”。先看页面骨架,从上到下依次是:

  1. 顶部导航栏:返回按钮、居中标题、右上角更多按钮
  2. 商品图轮播:一张全宽图片区域
  3. 价格区:价格左对齐、销量和收藏右对齐
  4. 商品信息卡片:标题、副标题、规格参数
  5. 底部操作栏:客服、收藏、加入购物车、立即购买

布局选型思路如下:顶部导航栏和底部操作栏都用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并实现IDataSource
ListItem点击事件不生效事件绑定在了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布局体系的理解会上一个台阶。

我在第一家公司的时候,带我的师傅说过一句话,后来我一直拿它当判断标准:好的布局代码,应该是别人拿到手之后,看结构就能猜到组件之间的从属关系。如果一段布局代码需要画半天草图才能讲明白,那它就不是好结构。今天讲的这些容器,本质上都是在帮你把“组件之间的关系”表达得更准确。多写、多拆、多试几次,这套手感就有了。

后面如果时间允许,我打算把“布局动画”和“自定义布局”这两个进阶方向也写成实操篇。布局静态结构搭好了,下一步就是让它动起来、活起来。各位先把今天的代码跑通,有问题可以在下面留言,看到都会回。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 5:02:56

多智能体系统核心架构与实战:从拓扑选型到工程避坑

上个月我负责的一个数据处理项目翻车了:四个智能体协作处理一批业务报表,结果两个智能体在“时间字段用什么格式”这个问题上反复争论,任务跑了整整六个小时,光token费用就抵得上一个初级员工一周的工资。复盘的时候我发现&#x…

作者头像 李华
网站建设 2026/9/11 5:02:04

100G UDP FPGA上板测试:系统级压力验证方法论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 4:58:54

Hadoop distcp命令原理与大数据迁移实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 4:56:17

固态硬盘品牌怎么选?前六品牌实测拆解与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华