HarmonyOS智能家居——UI布局模式的深度拆解

HarmonyOS智能家居——UI布局模式的深度拆解
写完五个页面后发现所有 UI 都是由五种基础布局Stack、Row、Column、Grid、Scroll嵌套组合出来的。这篇不讲怎么用布局API而是拆解项目里每个页面的具体嵌套结构搞清楚这行代码为什么放在这里、去掉会怎样。完整效果一、Stack层叠布局的三种用法Stack 在项目里出现了三种完全不同的用法每种都是底层放装饰、上层放内容的模式。用法一背景 内容层叠首页 HeaderStack(){// 底层渐变背景圆Column(){}.width(44).height(44).borderRadius(22).linearGradient({angle:135,colors:[[A,0],[#A78BFA,1]]})// 上层emoji 图标Text().fontSize(20)}Stack 的默认对齐是Alignment.Center——两层都居中叠放。底层是一个 44×44 的渐变圆Column 没有内容纯色块上层是 emoji。最终效果是一个渐变色圆形背景上叠了一个图标。为什么用 Stack 而不是给 Text 直接加背景因为 Text 的backgroundColor是矩形的——即使 Text 只有两个字符宽背景也是整个矩形区域。Stack 让底层可以用borderRadius(22)做成圆形上层 Text 不受底层形状影响。用法二状态卡片的渐变背景首页 Status CardStack({alignContent:Alignment.Center}){// 底层三色渐变背景Column(){}.width(100%).height(110).borderRadius(20).linearGradient({angle:135,colors:[[A,0],[#7C6FF0,0.5],[#A78BFA,1]]})// 上层设备状态文字 emojiRow(){Column(){Text(设备状态).fontSize(14).fontColor(rgba(255,255,255,0.7))Text(this.devCount/DEVICES.length 在线).fontSize(22).fontWeight(FontWeight.Bold).fontColor(Color.White)}.alignItems(HorizontalAlign.Start)Text().fontSize(44)}.width(100%).padding(20)}alignContent: Alignment.Center让上下两层都居中。底层是三色渐变紫 → 淡紫 → 浅紫上层是白色文字和 emoji。rgba(255,255,255,0.7)让设备状态标题半透明白Color.White让在线数全白——同色系但不同透明度形成标题和内容的层次。Stack 的外层容器.width(100%).height(110).borderRadius(20)设定了卡片的尺寸和圆角。这个尺寸同时约束了底层和上层——底层的渐变背景填满 110vp 高度上层的文字在这个范围内排列。用法三导航栏返回按钮的点击区域扩展Row(){Row(){SymbolGlyph($r(sys.symbol.chevron_left)).fontSize(20).fontColor([T1])}.width(34).height(34).borderRadius(17).backgroundColor(rgba(0,0,0,0.03)).justifyContent(FlexAlign.Center)Text(房间详情).fontSize(20)...layoutWeight(1)}虽然这行没有显式用 Stack但外层 Row 的作用和 Stack 类似——扩大了返回按钮的点击区域。SymbolGlyph 本身只有图标那么大约 16×16外层 Row 扩大到 34×34让用户不需要精确点击图标中心。如果外层不用 Row 而是直接给 SymbolGlyph 加paddingSymbolGlyph($r(sys.symbol.chevron_left)).fontSize(20).fontColor([T1]).padding(9)// 44 - 20 24, /2 12, 约 9vppadding 虽然扩大了点击区域但不会显示背景色。用 Row backgroundColor 能同时扩大点击区域和显示一个浅色底色视觉上更明确。二、Row Blank标题行的固定模式项目里所有标题 右侧信息的行都用同一个模式Row(){Text(房间).fontSize(17).fontWeight(FontWeight.Bold).fontColor(T1)Blank()Text(ROOMS.length 个).fontSize(12).fontColor(T2)}.width(100%).padding({left:16,right:16}).margin({bottom:10})逐行分析Text(房间).fontSize(17).fontWeight(FontWeight.Bold).fontColor(T1)——左侧标题17px 粗体深色。Blank()——弹性空白。这是 Row 里的关键组件——它占据所有剩余空间把左右两侧的文字推到两端。没有 Blank两个 Text 会紧挨在一起。Text(ROOMS.length 个).fontSize(12).fontColor(T2)——右侧信息12px 灰色。字号比标题小12 vs 17颜色比标题淡T2 vs T1形成视觉上的主次关系。.width(100%).padding({ left: 16, right: 16 })——行宽 100%左右各留 16px 边距。这个边距值和页面内其他元素卡片、列表的左右边距一致保证所有元素左对齐。这个模式在项目里出现了 8 次页面标题行内容Index房间 5 个Index今日能耗 2.8 kWhIndex智能场景RoomPage房间名 设备数EnergyPage今日能耗 2.8 kWhEnergyPage24小时用电分布EnergyPage各房间能耗EnergyPage节能建议每次都用Row Text Blank Text的结构只是标题和右侧内容不同。如果以后要统一修改标题行的样式比如改字号或间距需要改 8 个地方。抽成 Builder 可以解决BuilderSectionTitle(title:string,extra?:string){Row(){Text(title).fontSize(17).fontWeight(FontWeight.Bold).fontColor(T1)Blank()if(extra!undefined){Text(extra).fontSize(12).fontColor(T2)}}.width(100%).padding({left:16,right:16}).margin({bottom:10})}但当前没有抽取——因为 8 次重复的维护成本在可接受范围内且部分标题行有额外的 margin 或 padding 微调统一抽取需要处理这些差异。三、Column 的垂直间距控制方式一marginText(快捷控制).fontSize(17).fontWeight(FontWeight.Bold).fontColor(T1).width(100%).padding({left:16}).margin({bottom:10})margin 控制元素和相邻元素之间的距离。margin({ bottom: 10 })在标题下方留 10px 空间让标题和下方的 Grid 之间有间距。方式二Column({ space: N })Column({space:10}){this.RoomEnergy(️ 客厅,1.2,40)this.RoomEnergy(️ 卧室,0.8,28)this.RoomEnergy( 厨房,0.5,18)this.RoomEnergy(书房,0.2,8)this.RoomEnergy( 浴室,0.1,6)}space: 10让 Column 里的每个子元素之间自动加 10px 间距。比手动给每个子元素加 margin 更简洁——不需要在每个子元素上都写margin({ top: 10 })。方式三Blank().height(N)Column(){// ... 所有内容Blank().height(100)}空白填充。Blank().height(100)在内容底部加 100vp 空白保证最后一个卡片不会被底部导航栏遮挡。这个 100vp 约等于导航栏高度64vp 上方一些留白约 36vp。三种方式的使用场景方式场景优点缺点margin元素间距不均匀精确控制每个方向代码啰嗦Column space子元素间距均匀一行代码搞定只能统一间距Blank.height内容底部留白简单直观只适合底部填充首页的 Scroll Column 里混合使用了三种方式标题行用 margin每个标题的 margin 值可能不同房间卡片用 Row space间距统一 10底部用 Blank固定 100vp。这种混合使用是实际开发中的常态——没有一种方式能覆盖所有场景。四、横向 Scroll内容溢出的处理首页房间列表Scroll(){Row({space:10}){ForEach(ROOMS,(room:Room){Column(){Text(room.icon).fontSize(30).margin({bottom:8})Text(room.name).fontSize(13).fontWeight(FontWeight.Bold).fontColor(T1)Text(room.activeCount/room.deviceCount 设备).fontSize(10).fontColor(T2)}.width(100).padding(14).backgroundColor(CW).borderRadius(16).border({width:1.5,color:room.color30}).onClick((){router.pushUrl(...)})})}.width(100%).padding({left:16,right:16})}.width(100%).scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)逐层拆解最外层 Scroll——设定滚动方向为水平ScrollDirection.Horizontal隐藏滚动条BarState.Off。Scroll 的宽度是 100%但内容Row的宽度可能超过 100%——5 个房间卡片每个 100vp 10vp 间距 550vp超过屏幕宽度。Scroll 让超出部分可以滑动查看。中间层 Row({ space: 10 })——所有卡片横向排列间距 10vp。Row 的宽度是 100%但这不意味着它只占 100%——Row 的实际宽度由子元素决定当子元素总宽度超过 Row 的 100% 设定时Row 会超出 Scroll 的可见区域触发滚动。内层 Column——每个卡片是固定 100vp 宽的纵向布局。.width(100)保证所有卡片等宽不管内容emoji、名称、设备数长度如何。padding({ left: 16, right: 16 })——Row 左右各留 16px 边距。这保证第一个卡片不会紧贴屏幕左边缘最后一个卡片不会紧贴右边缘。但注意当滚动到最后一个卡片时右边距可能不够——这是横向 Scroll 的常见问题可以通过给 Row 最后加一个空白占位解决。首页场景列表Scroll(){Row({space:10}){ForEach(this.scenes,(s:Scene,idx:number){Column(){Text(s.icon).fontSize(28).margin({bottom:6})Text(s.name).fontSize(11).fontWeight(FontWeight.Bold).fontColor(T1)Text(s.active?运行中:点击启动).fontSize(9).fontColor(s.active?s.color:T2).margin({top:2})}.width(90).padding(12).backgroundColor(s.active?s.color15:CW).borderRadius(14).border({width:s.active?1.5:1,color:s.active?s.color:#F0EEF4}).onClick((){this.activateScene(idx)})})}.width(100%).padding({left:16,right:16})}.width(100%).scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)和房间列表的结构几乎一样——Scroll Row ForEach Column。区别是卡片宽度90 vs 100和卡片内容场景有状态文字房间有设备数。两个横向 Scroll 的共同结构Scroll(水平滚动, 隐藏滚动条) └→ Row(space: 10, 左右 padding: 16) └→ ForEach(数据数组) └→ Column(固定宽度, padding, 圆角, 点击事件)这个结构可以作为横向卡片列表的标准模板适用于任何需要横向滚动的场景。五、Grid 的 2×2 布局Grid(){GridItem(){Row(){Text().fontSize(22).margin({right:10})Text(客厅主灯).fontSize(12).fontColor(T1).layoutWeight(1)Text(DEVICES[0].on?:⚪).fontSize(16)}.width(100%).padding(12).backgroundColor(CW).borderRadius(14).onClick((){toggleDevice(1);this.devCountgetActiveCount()})}GridItem(){/* 客厅空调 */}GridItem(){/* 电视 */}GridItem(){/* 床头灯 */}}.columnsTemplate(1fr 1fr).columnsGap(10).rowsGap(10).width(100%).padding({left:16,right:16}).margin({bottom:20}).columnsTemplate(1fr 1fr)——定义两列等宽。1fr是弹性单位表示剩余空间的一份。两个1fr表示两列平分可用宽度。去掉 padding 后每列宽度 (屏幕宽度 - 32px 左右边距 - 10px 列间距) / 2。.columnsGap(10).rowsGap(10)——列间距和行间距都是 10vp。如果只设columnsGap不设rowsGap行间距默认为 0两行卡片会紧挨在一起。每个 GridItem 内部是 Row——emoji 名称 状态指示器横向排列。layoutWeight(1)让名称占据所有剩余空间状态指示器固定在右侧。.width(100%)让 Row 填满 GridItem 的宽度。为什么不直接用 Column Row 组合// 也可以实现 2×2 布局Row(){Column(){/* 客厅主灯 */}.layoutWeight(1)Column(){/* 客厅空调 */}.layoutWeight(1)}.margin({bottom:10})Row(){Column(){/* 电视 */}.layoutWeight(1)Column(){/* 床头灯 */}.layoutWeight(1)}手动嵌套 Row Column 也能做 2×2但代码更啰嗦——需要自己管理每行的间距、每列的宽度。Grid 通过columnsTemplate自动处理这些代码更简洁。当列数或行数变化时比如改成 3 列Grid 只需要改columnsTemplate(1fr 1fr 1fr)手动嵌套需要重构整个布局。六、Stack(alignContent) 的对齐控制项目里 Stack 出现了两种对齐方式默认对齐Alignment.CenterStack(){Column(){}.width(44).height(44).borderRadius(22).linearGradient(...)Text().fontSize(20)}没有指定alignContent默认是Alignment.Center——所有子元素居中叠放。适合背景 图标这种简单层叠。显式对齐Alignment.BottomStack({alignContent:Alignment.Bottom}){Scroll(){Column(){/* 所有内容 */}}Row(){/* 底部导航栏 */}}首页用Alignment.Bottom让导航栏固定在底部Scroll 内容在上方滚动。Stack 的高度是 100%Scroll 和 Row 都在同一层叠放Bottom 对齐让 Row 贴底。如果改成默认的 Center 对齐导航栏会跑到屏幕中间——因为 Row 的高度只有 64vp居中后会在屏幕中间位置上下各有大量空白。对齐方式的选择场景对齐方式原因背景 前景Center默认两层都居中不需要额外配置固定头部/底部Bottom 或 Top内容层滚动固定层贴边全屏覆盖Center 或 Stretch弹窗/蒙层需要覆盖整个屏幕七、整体页面的嵌套结构模板五个页面的嵌套结构可以归纳为一个通用模板Stack({ alignContent: Alignment.Bottom }) // 层叠容器 ├→ Scroll() // 可滚动内容 │ └→ Column() // 垂直排列 │ ├→ 导航栏 Row // 固定顶部 │ ├→ 卡片/列表区域 // 核心内容 │ │ ├→ 标题行 (Row Text Blank Text) │ │ ├→ 内容区 (ForEach / Grid / Scroll) │ │ └→ 分割空白 (Blank().height) │ └→ 底部留白 (Blank().height(100)) └→ Row() // 固定底部首页导航栏 └→ NavBtn × 5首页有底部导航栏用 Stack Alignment.Bottom 把导航栏固定在底部。其他四个页面没有底部导航栏直接用Column Scroll就够了Column() // 页面容器 ├→ 导航栏 Row // 固定顶部 └→ Scroll() // 可滚动内容 └→ Column() // 垂直排列 ├→ 卡片/列表区域 └→ 底部留白这个模板的规律导航栏不参与滚动始终固定在顶部。首页通过 Stack 实现固定其他页面通过把导航栏放在 Scroll 外面实现固定。两种方式的视觉效果一样——用户滚动内容时导航栏始终可见。八、布局选择的决策树需要展示内容 ├→ 内容能一屏放下 │ ├→ 是 → Column垂直排列 │ └→ 否 → Scroll Column垂直滚动 ├→ 内容需要层叠 │ ├→ 是 → Stack装饰层 内容层 │ └→ 否 → Row / Column线性排列 ├→ 内容是等宽卡片 │ ├→ 数量固定 → Grid自动对齐 │ └→ 数量不固定 → Scroll Row横向滚动 └→ 内容需要固定头部/底部 ├→ 是 → Stack Alignment.Bottom/Top └→ 否 → 直接用 Column实际开发中大多数页面是这些布局的组合——Stack 包裹 ScrollScroll 包裹 ColumnColumn 里嵌套 Row 和 Grid。理解每种布局的职责Stack层叠、Row横向、Column纵向、Grid网格、Scroll溢出滚动就能快速判断该用哪种组合。