ARTICLE DETAIL

资讯详情

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

HarmonyOS 应用实战:校园树洞投稿(四)我的——渐变头图、负 margin 悬浮卡与数据聚合

HarmonyOS 应用实战:校园树洞投稿(四)我的——渐变头图、负 margin 悬浮卡与数据聚合 HarmonyOS 应用实战校园树洞投稿四我的——渐变头图、负 margin 悬浮卡与数据聚合项目编号42-treehole-post 技术栈HarmonyOS ArkTS · ArkUI 声明式 UI 本篇页面我的ProfileTab.ets100 行 系列导航一首页 · 二投稿 · 三话题 ·四我的源码开源https://gitee.com/codenestFlow/HarmonyOSHub一、本篇聚焦在四页数据流里见 一首页 第十节我的页是统计端它把HoleStore里标记为mine的投稿聚合成三个数字再把收藏、消息、设置组织成入口列表。本页也是全应用视觉上最特殊的一页唯一使用渐变、唯一使用负 margin、唯一不需要固定 Header——因为头图本身从屏幕顶端开始延伸。本篇拆三件事渐变头图与悬浮卡的几何关系、负 margin 的层叠原理、三个静态统计数字如何变成按mine聚合的真实值。二、数据模型两个配置驱动的 interfaceinterface StatItem { value: string; label: string; } interface MenuItem { id: number; emoji: string; title: string; }两个 interface 都极小但定义方式值得注意value: string而不是number。这是一个明确的取舍——统计区当前只做展示不做运算直接存字符串省一层格式化代价是第七节聚合改造时必须换回number。展示型数据存 string、参与计算的数据存 number这条线要在建模时就想清楚返工时才知道改哪里。private stats: StatItem[] [ { value: 15, label: 投稿记录 }, { value: 328, label: 收到抱抱 }, { value: 42, label: 收藏 } ]; private menus: MenuItem[] [ { id: 1, emoji: , title: 投稿记录 }, { id: 2, emoji: ⭐, title: 我的收藏 }, { id: 3, emoji: , title: 收到的抱抱 }, { id: 4, emoji: , title: 互动消息 }, { id: 5, emoji: ⚙️, title: 设置 } ];菜单用配置数组而不是五个手写的Row增删菜单只改数组ForEach与分隔线逻辑第六节自动适配。stats三项与menus前三项语义一一对应投稿记录 / 收到抱抱 / 收藏是同一份数据的两种视图。三、页面骨架唯一不固定 Header 的一页build() { Column() { Scroll() { Column() { this.UserCard() this.StatRow() this.MenuList() this.About() } .width(100%) .padding({ bottom: D.pad this.safeBottom 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) }对比前三页的骨架Header()在Scroll之外本页把UserCard()放进了Scroll之内是四个页面中唯一不固定 Header 的一页。原因在头图的定位UserCard的紫色渐变要从屏幕顶端状态栏后面一直铺到下方约 200vp 处统计卡再悬浮其上。如果它固定在Scroll外内容滚动时渐变不动、只有下面滚——悬浮关系还在放进Scroll内整块头图随内容滚走页面更像一个整体。两种都成立本页选后者是因为头图本身是内容的一部分承载身份区而不是工具栏。align(Alignment.Top)在这里同样关键Scroll内容不足一屏时若无此对齐内容会垂直居中——悬浮卡就飘在屏幕中间了。四、UserCard线性渐变的色标与半透明头像Builder UserCard() { Row({ space: 14 }) { Row() { Text().fontSize(32) } .width(64).height(64).backgroundColor(#33FFFFFF).borderRadius(32).justifyContent(FlexAlign.Center) Column({ space: 5 }) { Text(树洞旅人).fontSize(18).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) Text(投稿 15 次 · 收到 328 个抱抱).fontSize(12).fontColor(#FFFFFF).opacity(0.85) }.alignItems(HorizontalAlign.Start).layoutWeight(1) } .width(100%) .padding({ top: this.safeTop 30, left: D.pad, right: D.pad, bottom: 44 }) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) }五个细节angle: 135的对角渐变—— 135 度即左上→右下。C.primary#6C5CE7 深在左上、C.accent#9B8AF5 浅在右下左上暗右下亮视觉重心落在左侧头像上。色标是[color, position]数组——[[C.primary, 0.0], [C.accent, 1.0]]position 取 0.0~1.0。两端之外自动钳制中间线性插值想加中间色只需插一项[C.accent, 0.5]。#33FFFFFF半透明白头像底——33是十六进制 alpha约 20%。在深色渐变上叠一层 20% 白既区分于背景又保持同色系比写死白色柔和得多。这是在彩色背景上放中性容器的标准手法。头像 64×64 borderRadius(32)—— 圆角是尺寸的一半得到正圆justifyContent(FlexAlign.Center)让 居中。副文案opacity(0.85)—— 白字直接降不透明度做次级比引入新色值干净。还剩一个数字padding({ bottom: 44 })——它不是随手写的见下一节。五、StatRow负 margin 悬浮的几何与层叠Builder StatRow() { Row() { ForEach(this.stats, (s: StatItem) { Column({ space: 4 }) { Text(s.value).fontSize(18).fontWeight(FontWeight.Bold).fontColor(C.text) Text(s.label).fontSize(11).fontColor(C.textDim) }.layoutWeight(1) }, (s: StatItem) s.label) } .width(92%).padding({ top: 14, bottom: 14 }) .backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }).margin({ top: -24 }) }悬浮的几何账UserCard底部 padding 44StatRow顶部 margin -24两者在文档流中先后排列UserCard 渐变区实际高度 内容高 top padding bottom padding(44) StatRow 顶部位置 UserCard 底边 - 24负 margin 上提 → 统计卡上边缘侵入渐变区 24vp卡片总高 141841114 ≈ 61 → 渐变区可见的卡片下缘留白 44 - 24 20vp也就是说bottom: 44不是孤立的值它减去负 margin 的 24剩下的 20 是渐变区在卡片下方的可见高度。三个数字44、-24、20满足44 24 20改任何一个另外两个都要跟着算——这种耦合的几何建议在代码里加一行注释写明关系否则半年后自己都不知道 44 从哪来。层叠顺序StatRow在UserCard之后声明默认绘制在其上层。ArkUI 的兄弟节点按声明顺序渲染后者覆盖前者——不需要zIndex。若顺序写反负 margin 会把卡片压到渐变下面看起来消失了一半。宽度与居中width(92%)让左右各留 4% 呼吸边距。注意它居中依赖容器对齐——如果哪天发现卡片贴到了左边先检查外层Column的alignItems。为什么悬浮卡好于平铺统计数字属于身份的量化视觉上应该贴着头图平铺式头图一块、统计一块会在两块之间出现一条渐变底色的横条悬浮式则用一个负 margin 就完成了卡片从渐变里长出来的层次。六、MenuListindex 驱动的条件分隔线Builder MenuList() { Column() { ForEach(this.menus, (m: MenuItem, idx: number) { Row({ space: 12 }) { Text(m.emoji).fontSize(18) Text(m.title).fontSize(14).fontColor(C.text).layoutWeight(1) Text().fontSize(14).fontColor(C.textDim) } .width(100%).padding({ left: 14, right: 14, top: 14, bottom: 14 }) .onClick(() { promptAction.showToast({ message: m.title }); }) if (idx this.menus.length - 1) { Divider().color(C.stroke).strokeWidth(0.5) } }, (m: MenuItem) m.title) } .width(92%).margin({ top: 16 }).backgroundColor(C.card).borderRadius(D.rMd).border({ width: 1, color: C.stroke }) }三个可迁移的手法if (idx this.menus.length - 1)—— 在ForEach的 item 构建器里做条件渲染最后一条后面不出分隔线。分隔线跟着数据走菜单增删不需要人肉数行数。Divider().strokeWidth(0.5)—— 0.5 的物理细线在视觉上比 1 更轻C.stroke与卡片描边同色列表内部和卡片边缘的线是同一套语言。整行热区——onClick挂在Row上且 padding 14 铺满整行点哪都能进Text(m.title).layoutWeight(1)把箭头推到最右和首页的Blank()是同一个两端对齐家族的两种写法layoutWeight吃剩余空间Blank()占剩余空间。演进菜单项带徽标。互动消息未读数是设置页外最有价值的数字配置数组加一个可选字段即可interface MenuItem { id: number; emoji: string; title: string; badge: number; } private menus: MenuItem[] [ { id: 1, emoji: , title: 投稿记录, badge: 0 }, { id: 2, emoji: ⭐, title: 我的收藏, badge: 0 }, { id: 3, emoji: , title: 收到的抱抱, badge: 0 }, { id: 4, emoji: , title: 互动消息, badge: 3 }, { id: 5, emoji: ⚙️, title: 设置, badge: 0 } ]; // Row 内、箭头之前 if (m.badge 0) { Text(m.badge 99 ? 99 : m.badge.toString()) .fontSize(10).fontColor(#FFFFFF).backgroundColor(C.danger) .borderRadius(9).constraintSize({ minWidth: 18 }) .padding({ left: 5, right: 5, top: 1, bottom: 1 }) }if (m.badge 0)条件渲染没消息的项不占位布局零扰动99封顶避免三位数把徽标撑成胶囊。七、从静态字符串到真实聚合myStat()第二节埋的雷在这里拆value: string和写死的 15/328/42在投稿页发出一条树洞后纹丝不动。接上HoleStore后三个数字全部活起来// common/HoleStore.ets节选完整结构见首页篇第十节 export interface MyStat { posts: number; warm: number; favs: number; } const BASE_POSTS: number 15; // 历史投稿基线Demo 种子 const BASE_WARM: number 328; // 历史收到抱抱基线 const FAVS: number 42; // 收藏数收藏仓库另有实现此处取常量 export class HoleStore { static myStat(): MyStat { let posts: number BASE_POSTS; let warm: number BASE_WARM; for (const h of HoleStore.list) { if (h.mine) { posts posts 1; // 我投的稿 1 warm warm h.warm; // 收到的抱抱累加 } } return { posts: posts, warm: warm, favs: FAVS }; } }我的页侧的改造接口从string换回number渲染时再格式化StorageProp(holeVersion) version: number 0; // 订阅数据版本首页篇第十节 State stats: StatItem[] []; aboutToAppear(): void { this.reload(); } private reload(): void { const s: MyStat HoleStore.myStat(); this.stats [ { value: s.posts.toString(), label: 投稿记录 }, { value: s.warm.toString(), label: 收到抱抱 }, { value: s.favs.toString(), label: 收藏 } ]; } // UserCard 副标题同步改为 Text(投稿 s.posts 次 · 收到 s.warm 个抱抱)两条聚合纪律与话题页三话题 第七节一脉相承基线 增量15 mine 数而不是直接数全量。种子数据不是我投的mine: false只有投稿页publish()落库的才带mine: true全量计数会把 15 个历史投稿也数进去双算。聚合一处收口myStat()返回的MyStat同时驱动统计卡三格和头图副标题一处计算两处消费——若两处各自 for 循环将来改口径必然只改一处。闭环由此打通投稿页发一条「治愈瞬间」→publish()落库mine: true→bump()版本 1 → 我的页重拉 → 统计卡15 → 16、头图副标题同步、话题页1102 → 1103、首页列表顶部多一条。四个页面一个数据源一次投稿全部响应。八、About收尾的呼吸感Builder About() { Column({ space: 4 }) { Text(校园树洞投稿).fontSize(12).fontColor(C.textDim) Text(Version 1.0.0).fontSize(11).fontColor(C.textDim).opacity(0.7) }.width(100%).margin({ top: 24 }) }应用名 12 号 版本 11 号再叠 0.7 透明度——三级递减卡片文字 → About 一行 → 版本号让页尾自然淡出。margin({ top: 24 })比菜单区内部的 14 更大用间距暗示内容到此为止。版本号读自app.json5的versionName更规范硬编码便于 Demo 但发版前必须换成构建注入。九、实机验证1. 首屏紫色渐变头图从状态栏顶端铺开白色统计卡侵入渐变 24vp 悬浮负 margin 生效92%宽两侧留呼吸边距五项菜单完整。2. 点收到的抱抱→ Toast「收到的抱抱」。整行热区生效layoutWeight(1)把箭头顶到最右。3. 点互动消息→ Toast「互动消息」。五个菜单项闭包参数各自独立无串扰。4. 完整视图滚动到底About校园树洞投稿 / Version 1.0.0居中淡出底部安全区留白足够。验收清单项验证方式结果沉浸式头图渐变是否从屏幕顶端开始是padding top: safeTop 30内容避开状态栏负 margin 悬浮统计卡是否侵入渐变是-24 生效层叠在上三格等分layoutWeight(1)等宽且数字居中对齐条件分隔线数菜单分隔线4 条5 项 - 最后一条92% 居中统计卡/菜单卡位置左右对称底部安全区滚到底 About未被导航条遮挡十、可演进方向方向现状改法收益统计聚合写死 15/328/42myStat()按mine聚合投稿后数字实时 1头图副标题硬编码 15 次同源MyStat一处计算两处消费消息徽标无badge字段 条件渲染未读数一眼可见头像 占位Image 圆形裁剪 匿名默认图匿名产品也要有默认身份感退出登录无设置页内提供匿名身份可放弃数据可清数据清空无设置页「清空本地数据」 二次确认隐私控制权交还用户最后两项值得展开一句树洞的信任来自匿名 可销毁。用户应能一键清空自己的投稿痕迹本地mine数据 Preferences 草稿且删除必须二次确认、不可恢复提示到位——这是匿名社区的产品底线也是安全评审占比 20%最容易扣分的缺口。十一、本篇小结我的页 100 行里有三个真正需要想清楚的决定头图放进Scroll内它是内容不是工具栏、负 margin 的几何账44 24 20耦合值要写注释、兄弟节点声明顺序即层叠顺序悬浮卡压在渐变上一行zIndex都不用。第七节把三个静态字符串换成myStat()聚合后四篇的数据流在此闭环一个HoleStore、一个版本号、四个页面各取所需。系列到此完结首页消费、投稿生产、话题聚合、我的统计——同一个数据源在四只手里的四种用法。这套小仓库 订阅重绘的模式从 Demo 到真实后端HoleStore换成远程数据仓库 拉取失败兜底几乎可以原样迁移这也是四篇文章真正想留下的东西。源码开源https://gitee.com/codenestFlow/HarmonyOSHub
返回列表