拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 6卡片布局实战:用ArkUI从List到WaterFlow构建流畅界面

HarmonyOS 6卡片布局实战:用ArkUI从List到WaterFlow构建流畅界面 1. 先想清楚卡片布局的设计思路与选型做鸿蒙开发的老哥们应该都有体会卡片布局这玩意儿在HarmonyOS 6里已经是信息展示的绝对主力了。不管是应用首页的推荐流、金融App的资产总览还是IoT设备的管理面板一眼望去全是卡片的天下。为什么因为卡片这东西天生就是信息容器它能在一个独立区块里把图标、标题、描述、操作按钮这些冗杂的东西整合得井井有条用户扫一眼就能抓到重点交互路径也短——点击卡片进入详情几乎不需要思考。但正因为人人都用卡片如果你只是简单地往页面里堆几个圆角矩形那做出来的东西跟别人有什么区别我在HarmonyOS 6上实际开发过几个信息展示类页面之后最大的感触是卡片布局看起来简单真正要做好难点根本不在画几个圆角框而在布局策略的取舍、组件的合理搭配、以及数据渲染效率的掌控。这篇就来掰扯掰扯怎么用ArkUI声明式语法从零到一搭出一套能看的卡片界面。这套经验适合谁一是刚接触ArkUI、想搞清楚List、Grid、Row、Column这些基础组件到底怎么搭配使用的初学者二是已经在写业务代码但总觉得自己的页面差点意思想在布局结构、圆角阴影、响应式适配这些细节上有所提升的开发者。HarmonyOS 6的ArkUI组件能力和API接口相比早期版本完善了不少但核心的布局思路是通用的你拿去适配别的版本问题也不大。好先别急着写代码布局这东西顺序很重要。咱们先看设计层面的东西。2. 卡片布局的核心设计思路从哪来2.1 先定信息层级卡片里到底放什么很多新手一上来就铺开组件开写结果卡片里一会儿塞图、一会儿塞文字最后排版乱了信息也糊了。我一般习惯先问自己一个问题这张卡片对用户来说最重要的信息是什么举个例子一个典型的待办事项卡片核心信息是任务标题和截止时间次要信息是标签和优先级操作入口是完成和删除按钮。那设计的时候就要有层级第一层级任务标题字号最大加粗第二层级截止时间和状态标签第三层级操作按钮放在右下角视觉上不抢焦点这一步搞清楚了后面的组件嵌套才有方向。HarmonyOS 6的ArkUI是声明式写法结构就是嵌套关系你在纸上画清楚的信息层级直接映射到Column、Row的嵌套结构里代码写起来非常顺手。我自己的经验是每次写布局之前先草画一个信息层级图哪怕只是在备忘录里列几行也能避免50%以上的返工。毕竟代码能重构但信息架构想错了改起来伤筋动骨。2.2 布局容器选型List、Grid还是Scroll搭配组件卡片布局的第一大选择就是用哪个容器来装卡片。HarmonyOS 6的ArkUI里常见的方案有这么几种方案适用场景优点缺点List ListItem单列卡片流列表滚动懒加载机制成熟滚动性能好多列布局不够灵活Grid GridItem多列卡片网格瀑布流网格规则适合宫格和双列跨列合并比较复杂Scroll Column/Row卡片数量少、内容固定结构自由嵌套随意数据量大时性能堪忧WaterFlow瀑布流、卡片高度参差高度不一致时表现优秀版本要求相对较高拿最常见的首页推荐流来说卡片基本是上下滚动、一列到两列我首选List。为什么因为List在HarmonyOS里的懒加载机制最成熟配合LazyForEach几千条数据滑起来也不会卡。你要是用Scroll包Column数据一多所有子组件全部同帧创建性能直接就崩了。多列的话卡片高度一致优先上Grid高度差异大的内容用WaterFlow。HarmonyOS 6的WaterFlow时序细节不少建议先保留在Grid验证过需求后再考虑切换。2.3 卡片间距和留白决定精致感的隐藏因素很多人做卡片觉得为什么我的页面挤得慌别人的看起来就很透气——问题多半出在间距上。卡片布局不是把卡片一个挨一个码上去就行间距是精致感的关键。HarmonyOS 6的ArkUI里间距控制主要有三个手段在容器上统一设置space属性List、Grid都有同时控制好外边距margin两层有叠加关系每张卡片内部用padding撑出内容与边框的呼吸空间用间距组件比如空白行做局部微调我参考了实际开发里比较通用的参数页面左右留白页面级margin12vp~16vp卡片间距12vp左右双列卡片还会更大一些卡片内边距16vp上下左右16vp也是安全值卡片的圆角半径12vp到16vp之间小卡片8vp大卡片16vp这不是拍脑袋定的12vp的间距在大部分场景下既能保证卡片之间的视觉分隔又不会让页面显得太空洞。大家可以在这些基准值上微调。顺带说一句卡片圆角不要所有地方都用同一个值不同层级的卡片用不同的圆角能增加层次感。页面中重要性最高的大卡片用16vp小的操作卡片用8vp视觉上会更精致。这是我试过之后觉得提升质感最快的一招。3. 从零搭建HarmonyOS 6卡片实战步骤拆解好了思路理清了咱们直接上手。假设要做一个运动健康类App的首页包含一张用户今日运动数据总览卡、一张功能快捷入口卡、一个横向运动的列表卡区。这个案例很典型几乎涵盖了卡片布局的所有基础点。3.1 准备数据模型先定义好卡片的内容来自哪卡片布局不是光写UI就完事儿数据怎么来、怎么渲染同样要提前想清楚。这里用TypeScript定义一个运动数据模型// 运动数据模型 export interface SportData { id: string; title: string; // 运动项目名 value: string; // 展示数值如步数 8542 unit: string; // 单位如 步 trend: string; // 趋势描述如 较昨日 12% icon: ResourceStr; // 图标资源 }为什么要用interface来定义因为ArkUI的LazyForEach懒加载渲染要求数据源有唯一ID我示意里用的id字段就是为此服务的。实际项目中建议让数据源继承IDataSource、实现IDataSource的totalCount()、getData(index)等方法这是懒加载正确工作的前提。3.2 用List搭建卡片流的骨架接下来就是用List搭整个页面的滚动骨架。这个骨架相当于卡槽每张卡片最终都是ListItem里的内容。// 卡片流页面骨架 Entry Component struct SportsHomePage { private dataList: ArraySportData this.loadSportData(); build() { List({ space: 12 }) { // 头部卡今日运动总览 ListItem() { this.TodaySummaryCard() } // 快捷功能入口卡 ListItem() { this.QuickActionsCard() } // 运动项目列表用 LazyForEach 渲染 LazyForEach(this.dataList, (item: SportData) { ListItem() { this.SportItemCard(item) } }, (item: SportData) item.id) } .width(100%) .height(100%) .backgroundColor(#F1F3F5) .padding({ left: 12, right: 12, top: 8 }) } // 加载模拟数据 private loadSportData(): ArraySportData { // 构建测试数据 return [ { id: 1, title: 户外跑步, value: 5.2, unit: km, trend: 较昨日 18%, icon: $r(app.media.ic_run) }, { id: 2, title: 骑行, value: 12.8, unit: km, trend: 较昨日 5%, icon: $r(app.media.ic_ride) }, { id: 3, title: 力量训练, value: 45, unit: min, trend: 较昨日 -8%, icon: $r(app.media.ic_strength) } ]; } }这里有几个实操要点space的值设为12加上页面侧的padding也是12视觉上整个页面卡片间距保持统一节奏backgroundColor给整个页面加了一层浅灰底跟白色卡片形成对比让卡片浮出来LazyForEach是懒加载渲染的关键10条数据可能体验不出差别数据到几百条以后列表滚动的流畅度差距会非常明显3.3 ColumnRow嵌套写出一张有层次感的卡片骨架搭好了现在写第一张卡片今日总览。这里会用到Column纵向排列、Row横向排列、Text和Image几乎每天都在用的基础组件但嵌套时要多留个心眼。// 今日运动总览卡 Builder TodaySummaryCard() { Column({ space: 12 }) { // 卡片头部标题 跳转按钮 Row() { Text(今日运动总览) .fontSize(14) .fontWeight(FontWeight.Bold) .fontColor(#141414) Blank() Text(查看详情 ) .fontSize(12) .fontColor(#4A7CF7) } .width(100%) // 核心数据区 Row({ space: 14 }) { // 步数 Column({ space: 4 }) { Text(8542) .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor(#141414) Text(今日步数) .fontSize(12) .fontColor(#999999) } .layoutWeight(1) // 分割线视觉分隔 Divider() .vertical(true) .height(36) .color(#EEEEEE) // 卡路里 Column({ space: 4 }) { Text(368) .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor(#141414) Text(千卡) .fontSize(12) .fontColor(#999999) } .layoutWeight(1) Divider() .vertical(true) .height(36) .color(#EEEEEE) // 运动时长 Column({ space: 4 }) { Text(46) .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor(#141414) Text(分钟) .fontSize(12) .fontColor(#999999) } .layoutWeight(1) } .width(100%) .alignItems(VerticalAlign.Bottom) } .padding(16) .backgroundColor(Color.White) .borderRadius(16) .shadow({ radius: 8, color: rgba(0, 0, 0, 0.06), offsetY: 4 }) }把卡片拆开看就是三个层次外层Column负责把整张卡片变成白色圆角圆角矩形同时用内边距16控制内容不贴边头部Row标题靠左、操作入口靠右用Blank()撑开双方距离数据区Row三个指标横向排布Divider做竖向分隔线这里有个常见问题必须提一下Blank()和layoutWeight都可以用来占位/分配空间但应用场景完全不同。让标题靠左、链接靠右用Blank()让三个数据列均分宽度用layoutWeight(1)。如果你把Blank()拉进来模拟均分效果代码维护会很痛苦数据项一变布局就崩。3.4 卡片上的点击反馈onClick别只调个方法就完事卡片本质上是可点击的容器那点击反馈就必不可少。但很多开发的实现只是简单挂一个onClick点击时页面完全没有任何视觉响应用户会觉得这玩意儿是不是挂掉了。HarmonyOS 6的ArkUI中我习惯的做法是配合stateStyles实现按压态// 卡片容器加入按压态反馈 Column({ space: 12 }) { // ... 卡片内容 } .padding(16) .backgroundColor(Color.White) .borderRadius(16) .stateStyles({ pressed: { .scale({ x: 0.98, y: 0.98 }) } // 按压时轻微缩小 })用stateStyles的pressed状态配合scale属性能在不写额外状态变量的前提下轻松实现按压缩放效果。0.98这个缩放值别太小太小的缩放弹来弹去反而显得廉价也别太大太大会用户无感知。3.5 给列表加个头懒加载数据源的正确姿势上面骨架里用了LazyForEach但它要求数据源实现IDataSource接口这里顺手写一下。// 实现一个简单的懒加载数据源 class SportDataSource implements IDataSource { private data: ArraySportData []; private listeners: DataChangeListener[] []; constructor(data: ArraySportData) { this.data data; } totalCount(): number { return this.data.length; } getData(index: number): SportData { return this.data[index]; } registerDataChangeListener(listener: DataChangeListener): void { this.listeners.push(listener); } unregisterDataChangeListener(listener: DataChangeListener): void { const index this.listeners.indexOf(listener); if (index ! -1) { this.listeners.splice(index, 1); } } }实际项目中还会写addData、deleteData等方法并在增删时调用listener.onDataAdd、onDataDelete去通知UI更新。这里先保持最小实现。为什么要这么麻烦因为IDataSource的存在LazyForEach才知道需要渲染哪些item。滚动时它按需创建组件、滑出屏幕的item组件会被回收复用。如果数据变化了你没通知它列表就会显示错乱或渲染过期数据。踩过这个坑的人应该都懂我说的是哪类bug。4. 双列卡片与瀑布流Grid和WaterFlow的实战要点单列卡片流做出来了接下来是很多应用会用到的双列卡片网格。同样是卡片放进Grid里和放进List里的处理逻辑会有一些差异值得单独说一说。4.1 Grid双列卡片均分宽度千万别手动算先看一段常见错误写法// 错误示例不推荐手动算宽 Grid() { // ... } .columnsTemplate(1fr 1fr) .columnsGap(12) .rowsGap(12)columnsTemplate里写1fr 1fr意思是两列均分可用宽度间距用columnsGap控制这是标准做法。但有些人会习惯性去手算卡片宽度比如用屏幕宽度除以2再减掉间距。别这么做不同屏幕、不同字体缩放比例下手算的宽度早晚会出问题。用fr单位的栅格模板来自动分配是正解。4.2 卡片高度不一致怎么办WaterFlow的坑与解双列卡片如果高度不一致比如左边卡片没有图、右边卡片有图Grid会强制同一行高度对齐露出来大片空白。这就要切换成WaterFlow。HarmonyOS 6里WaterFlow的基本写法WaterFlow() { LazyForEach(this.flowDataSource, (item: CardData) { FlowItem() { this.CardView(item) } }, (item: CardData) item.id) } .columnsTemplate(1fr 1fr) .columnsGap(12) .rowsGap(12)WaterFlow确实能顺滑地处理高度参差但要注意它不会像Grid那样帮你统一卡片宽度如果卡片内容自带背景色宽度撑不满FlowItem就会出现卡片有宽有窄的视觉问题。我的习惯是给FlowItem里的根容器加上.width(100%)确保所有卡片宽度一致。另外WaterFlow的滚动性能相对List和Grid还是要敏感一些卡片内容越复杂、数据量越大差异越明显。如果卡片高度差异不大优先用Grid高度确实参差才上WaterFlow。这个决策顺序能帮你避免不少性能坑。4.3 两种布局的动态切换监听容器宽度HarmonyOS 6主打一多适配一套代码适配手机、折叠屏、平板卡片布局在手机上是双列在平板上就要变成三列甚至四列否则卡片被拉得又宽又空信息密度就崩了。这里可以用Grid上的onGridItemIndexChange做精细控制也可以直接在组件里用MediaQuery监听断点// 监听容器宽度变化动态调整列数 State private currentBreakpoint: string sm; aboutToAppear() { MediaQuery.get((width 600vp)).on(change, (e) { if (e.matches) { this.currentBreakpoint lg; } else { this.currentBreakpoint sm; } }); }然后在Grid里根据断点返回不同的栅格模板Grid() { // ... } .columnsTemplate(this.currentBreakpoint lg ? 1fr 1fr 1fr : 1fr 1fr) .columnsGap(12) .rowsGap(12)对了MediaQuery别忘了在aboutToDisappear里移除监听否则页面销毁了回调还挂着轻则回调无效重则内存泄漏。这块我在实际项目里排查过几次大部分退出页面后还能收到监听事件的诡异问题都来自这里。5. 卡片融入交互与状态管理从静态走向动态布局做到这视觉层面已经能打了但真正的卡片布局实战避不开交互状态。多个卡片之间的交互联动和状态同步才是精美展示背后真正值钱的部分。5.1 选中态高亮用State驱动卡片外观比如一个训练计划列表用户点击某张卡片后卡片需要保持高亮状态直到点击另一张才取消。这种场景用State选中索引来驱动是最高效的。State selectedIndex: number -1; build() { List({ space: 12 }) { LazyForEach(this.dataList, (item: PlanData, index: number) { ListItem() { this.PlanCard(item, index) } }, (item: PlanData) item.id) } } Builder PlanCard(item: PlanData, index: number) { Column({ space: 8 }) { // 卡片内容 } .padding(16) .backgroundColor(this.selectedIndex index ? #EAF1FE : Color.White) .borderRadius(12) .border({ width: this.selectedIndex index ? 1 : 0, color: #4A7CF7 }) .onClick(() { this.selectedIndex index; }) .animation({ duration: 200, curve: Curve.EaseOut }) }这里有个关键点此前为唯一ID的渲染键LazyForEach里要用item的id比如(item: PlanData) item.id而不是index。如果你偷懒用index做key删除中间一条数据后后面的item会复用缓存的组件实例选中态就会错位。5.2 卡片内的表单状态提升用户体验的小细节当卡片内部有输入框、开关Toggle或滑块Slider时交互状态的管理也要单独处理。因为卡片可能被复用、滑出屏幕后被回收状态如果只放在组件内部回收重建后容易丢失。一个实用的方案是把状态提升到页面组件用State管理卡片中的所有可变值传入卡片的同时传入回调。这样无论卡片怎么复用、怎么销毁重建状态都还留在页面上。State switchStatus: boolean true; Builder SettingCard(switchStatus: boolean, onChange: (v: boolean) void) { Row() { Text(智能提醒) .fontSize(14) Blank() Toggle({ type: ToggleType.Switch, isOn: switchStatus }) .onChange((isOn: boolean) { onChange(isOn); }) } .padding(16) .backgroundColor(Color.White) .borderRadius(12) }5.3 状态变量的层级设计别一股脑全塞进State状态管理不仅关乎功能也直接影响UI性能和代码可维护性。一个通病是把所有字段全部声明成State结果数据一变化整棵树频繁重渲染。ArkUI的State是UI相关状态只有被UI读取过的属性变化才会触发刷新。卡片布局的身份建议如下State影响UI的局部状态比如选中索引、折叠/展开标记Prop/ObjectLink父子组件间的单向数据同步适合跨层卡片共享状态Provide/Consume跨层级通信适合多卡片、深嵌套场景如主题切换事件普通成员变量与UI无关的配置、常量放普通成员变量即可状态变量不一定是越少越好而是层级越清晰越好。一个经验原则能局部容器内解决的State到State不跨层必须跨层的优先Provide/Consume尽量避免State到State层层传递。6. 常见问题排查与避坑指南最后把这些年在HarmonyOS 6卡片布局上踩过的坑集中整理了一下全是基于实际开发经验的记录。问题现象排查思路解决方案圆角不生效卡片是个直角矩形borderRadius被父容器裁剪或覆盖检查嵌套结构里有没有设置裁剪clip(true)确认borderRadius写在最外层根容器上滚动列表卡顿掉帧列表没走懒加载全部一次性创建换LazyForEach并确保数据源实现IDataSource卡片点击后整个页面闪烁.animation加在了高代价布局属性上动画属性尽量收敛到transform、opacity这类不影响布局的属性双列卡片高度不一致导致空白Grid行的强制等高行为高度参差大换WaterFlow轻微差异用GridrowsGap微调字体缩放后卡片文字溢出字号设置使用了固定vp值改用预定义字体尺寸FontSize枚举或按容器宽度动态缩放深色模式下卡片刺眼背景颜色写死了Color.White用Resource资源引用通过深浅色资源自动切换卡片底部按钮被系统导航条遮挡安全区适配未处理页面根节点设置expandSafeArea或监听安全区Insets调整底部留白重点提一下卡片内容溢出的排查思路卡片之所以精美很重要的一点是内容不溢出、不截断、不换行怪异。Text组件要预判内容长度Text(item.desc) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis })超出卡片高度的内容卡片内放不下不要试图让卡片自适应长高那会破坏整列卡片的对齐优先限制行数配textOverflow做省略号处理保持卡片高度统一。真需要完整内容就让用户点击进入详情页。还有一个体验细节在使用ForEach还是LazyForEach上别纠结太久业务数据往往远不止几十条直接用LazyForEach是更稳的选择。前期省掉的几步代码后期可能带来成倍的性能问题。另外卡片图片加载建议用Image组件的alt、objectFit属性做好占位和裁剪网络图先降采样再填充布局避免OOM和卡顿。写在最后的一点心得卡片布局在HarmonyOS 6上能做的东西远不止以上这些像Swiper轮播式卡片、GridRow栅格系统做复杂版面这些都是可以深入玩下去的方向。但我个人的感受是布局框架学起来快真正拉开差距的是对间距、圆角、状态、性能这些细节神经的敏感度。同样的组件不同人嵌套出来的观感和手感完全不同。我在实际开发中最受益的一个习惯是每写完一张卡片都拿真机在不同尺寸和深色模式下面跑一遍截图看间距、看圆角、看溢出。屏幕上看着没问题的页面到了真机上往往能发现一堆此前没注意到的细节问题。卡片好写写好难。多写几个页面多踩几次坑你慢慢就会找到属于自己的那套卡片语言。
返回列表