ARTICLE DETAIL

资讯详情

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

鸿蒙原生实战:用 ArkUI 搭校园快递首页 —— 安全区适配与进度条列表

鸿蒙原生实战:用 ArkUI 搭校园快递首页 —— 安全区适配与进度条列表 鸿蒙原生实战用 ArkUI 搭校园快递首页 —— 安全区适配与进度条列表应用背景11 校园快递campus-express是鸿蒙原生校园工具系列的第 11 个应用采用橙色主题#EA580C整体走沉浸式全屏 安全区适配路线。本文逐行拆解HomeTab.ets共 310 行聚焦沉浸式全屏安全区适配、HeroCard 渐变统计卡、WeekChart 条形图、ExpressList 进度条列表四大核心组件并穿插讲解SearchBar、QuickAction、RankList等辅助模块的实现细节。首页是用户打开应用后第一个看到的页面承担着信息总览 快捷入口 动态推送三重职责。它不像寄件页那样强交互也不像取件页那样重时间轴而是把最能体现产品价值的关键数据用一张渐变头部卡 两个列表的形式平铺在首屏。理解首页本质上是理解如何用最克制的布局把最多的高频信息讲清楚。沉浸式全屏是 11 号应用最鲜明的工程特征它不同于传统状态栏留白 内容区的保守做法而是让背景一路铺到屏幕最顶端状态栏文字直接浮在橙色之上。这么做视觉张力更强、品牌感更足代价是开发者必须手动处理安全区避让——而这条动态计算避让区 → 写入 AppStorage → 各页面用 StorageProp 读取的链路正是首页源码最值得反复揣摩的隐藏主线。后面每一节的布局注释里你都会看到safeTop/safeBottom的身影它们就是这条链路的末端。一、页面整体结构与沉浸式安全区适配首页的根结构是一个Column内部自上而下排布Header()与Scroll()。Scroll通过layoutWeight(1)占据除头部外的全部剩余空间并把内部Column的space设为 14让卡片之间保持统一呼吸感。最关键的细节在Scroll的padding上// HomeTab.ets — 页面根结构含安全区 Component export struct HomeTab { StorageProp(safeTop) safeTop: number 0; StorageProp(safeBottom) safeBottom: number 0; State searchText: string ; build() { Column() { this.Header() Scroll() { Column({ space: 14 }) { this.HeroCard(); this.SearchBar(); this.QuickAction(); this.WeekChart(); this.SectionTitle(快递动态, 全部 ); this.ExpressList(); this.SectionTitle(快递公司排行, 详情 ); this.RankList(); } .width(100%) .padding({ left: D.pad, right: D.pad, top: 14, bottom: D.pad this.safeBottom 20 }) } .layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top) } .width(100%).height(100%).backgroundColor(C.bg) } }本页最大的学习价值正是StorageProp安全区适配机制。safeTop和safeBottom两个值来自AppStorage由EntryAbility.onWindowStageCreate()中通过getWindowAvoidArea()动态计算写入。也就是说应用一旦进入沉浸式全屏系统状态栏会被界面淹没原本状态栏下方的空间需要开发者自己补偿。Scroll底部padding用了D.pad this.safeBottom 20的表达式既保留了卡片左右与底部的常规留白D.pad又额外叠加了safeBottom避让底部导航条最后加 20 让最后一张卡片不与导航条贴死。scrollBar(BarState.Off)关闭了滚动条align(Alignment.Top)保证内容从顶部对齐——这两行组合在内容可能超出一屏的列表页里是标配既不影响阅读又不会因滚动条闪烁而破坏沉浸式观感。顺带一提Column(...).height(100%)配合Scroll().layoutWeight(1)的写法保证了无论内容多长滚动区域始终恰好占满 Header 以下的所有空间。如果漏掉layoutWeight(1)Scroll 会按内容高度撑开在内容不足一屏时出现底部空白、内容超过一屏时又无法滚动的诡异状态。这种固定 Header 弹性滚动区的二分结构是几乎所有多 Tab 应用首页的骨架务必记牢。项目源码开源https://gitee.com/codenestFlow/HarmonyOSHub二、数据模型六类接口定义首页数据量不小作者把零散字段收敛成 6 个interface这是 ArkTS 声明式开发中非常值得借鉴的做法。先看核心几个interface Express { id: number; emoji: string; code: string; company: string; time: string; tag: string; rating: number; progress: number; } interface StatItem { value: string; label: string; color: string; } interface WeekBar { day: string; value: number; } interface RankItem { id: number; rank: number; name: string; value: string; emoji: string; percent: number; } interface QuickItem { emoji: string; label: string; }Express描述一条快递动态字段覆盖了谁发的、什么单号、什么公司、什么时间、什么状态、用户打几星、运输进度多少——一个列表项要呈现的所有维度一次性建模干净。StatItem用value/label/color三件套概括了 HeroCard 里的三组统计color字段预留了未来按状态变色的能力。WeekBar与RankItem则分别是条形图和排行榜的数据源。把数据先抽象成接口再让Builder只负责怎么画是数据驱动 UI的雏形。当产品经理想增减一个快递公司或调整一周数据开发者只需改数组不动任何布局代码——这正是组件化最大的红利。三、Header 标题栏与安全区Header是首页唯一的固定吸顶元素它没有用Scroll包裹因此始终可见。它只放了一个标题文字但padding处理得很讲究// Header — 吸顶标题栏 Builder Header() { Column() { Text(校园快递) .fontSize(22).fontWeight(FontWeight.Bold).fontColor(C.text) .width(100%) .padding({ top: this.safeTop 10, left: D.pad, right: D.pad, bottom: 12 }) } .width(100%).backgroundColor(C.card) }.padding({ top: this.safeTop 10 })让标题文字主动避让状态栏——这是沉浸式全屏下的标准补偿手法。safeTop随设备动态变化在刘海屏上它可能是 60在普通屏上可能仅 20。作者没有写死一个 Magic Number而是把StorageProp读到的值直接参与运算这意味着同一份代码在任意机型上都不会出现标题被状态栏压住或顶部留白过大的尴尬。此外Header背景用C.card白色卡片色与下方C.bg浅灰页面背景形成轻微层次让标题区像一块浮在列表上方的面板视觉上把品牌与内容做了区隔。这里还有一个值得注意的取舍Header没有放进Scroll因此它吸顶是天然成立的——只要它写在 Scroll 之前且不被滚动容器包裹就永远停留在原位。相较之下有些应用会把标题也丢进滚动区再用sticky逻辑吸顶代码复杂且容易在滚动抖动时闪烁。11 号首页选择最朴素的分置法用最小的认知成本换来了稳定的吸顶效果这在 demo 与中小型项目里通常是更优解。四、HeroCard 渐变头部统计卡这是首页的门面。它用linearGradient铺了一块橙→浅橙的渐变把站点名、三项统计、两个主操作全部压在一张卡里。// HeroCard — 头部统计卡 Builder HeroCard() { Column({ space: 16 }) { Row() { Text(菜鸟驿站 · 梅园站点).fontSize(13).fontColor(#FFFFFF).opacity(0.85) Blank() Text(本月统计).fontSize(11).fontColor(#FFFFFF).opacity(0.7) }.width(100%) Row({ space: 12 }) { ForEach(this.stats, (s: StatItem) { Column({ space: 4 }) { Text(s.value).fontSize(22).fontWeight(FontWeight.Bold).fontColor(#FFFFFF) Text(s.label).fontSize(11).fontColor(#FFFFFF).opacity(0.8) }.layoutWeight(1).alignItems(HorizontalAlign.Center) }, (s: StatItem) s.label) }.width(100%) Row({ space: 12 }) { Button(扫码取件) .fontSize(14).fontColor(C.primary).backgroundColor(#FFFFFF) .borderRadius(D.rSm).height(40).layoutWeight(1) .onClick(() { promptAction.showToast({ message: 扫码取件 }); }) Button(一键代取) .fontSize(14).fontColor(#FFFFFF).backgroundColor(#33FFFFFF) .borderRadius(D.rSm).height(40).layoutWeight(1) .onClick(() { promptAction.showToast({ message: 一键代取 }); }) }.width(100%) } .width(100%).padding(20).borderRadius(D.rLg) .linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] }) }渐变从主色C.primary#EA580C过渡到强调色C.accent#FB923C角度 135° 形成右上偏亮的对角线光感。ForEach(this.stats)把待取件 3 / 本月取件 12 / 代取费 ¥35三组数据横向均分layoutWeight(1)白字在橙色底上对比清晰。关于渐变角度有个小知识点ArkUI 的linearGradient用angle表示方向0° 指向正上方、顺时针增加。135° 意味着从左下指向右上所以亮色C.accent放在colors数组的末尾比例 1.0就落在右上方——这正好模拟了自然光从左上打来的高光感。如果你想做左上亮的反向光把 angle 改成 315° 即可。这种用角度控制光向的技巧在一切需要伪 3D 立体感的卡片上都能用。双按钮的设计语言差别明显扫码取件是白底橙字的实底按钮是主操作一键代取用#33FFFFFF33 是十六进制透明度约 20% 白做半透明底、白字是次操作。这种主实底、次描边/透明的搭配是移动端引导用户优先点击主按钮的通用范式。两个按钮都通过promptAction.showToast给出轻量反馈——在 demo 阶段用 Toast 替代真实跳转既演示了交互闭环又不引入多余的页面跳转复杂度。五、SearchBar 搜索栏搜索栏是首页通向查件能力的入口结构极简一个放大镜 emoji 加一个TextInput。// SearchBar — 运单号查询 Builder SearchBar() { Row({ space: 10 }) { Text().fontSize(18) TextInput({ placeholder: 输入运单号查询, text: this.searchText }) .backgroundColor(transparent).borderRadius(D.rSm).height(40) .layoutWeight(1).placeholderColor(C.textDim).placeholderFont({ size: 14 }) .onChange((val: string) { this.searchText val; }) } .width(100%).padding({ left: 14, right: 14 }) .backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) }两个细节值得记TextInput设backgroundColor(transparent)让输入框融进外层白卡片里不出现双层背景onChange把输入实时写回State searchText这是 ArkUI 单向数据流的标准写法——数据往下流、事件往上回。.border({ width: 1, color: C.stroke })用描边而非阴影来定义卡片边界这与整个应用的扁平化设计语言保持一致。D.rMd是D尺寸类里的中圆角常量所有同级卡片共用保证圆角尺度统一。输入法IME的弹出也是首页需要考虑的隐式问题当TextInput获焦时系统键盘会顶起整个布局。由于SearchBar不在滚动区顶部键盘弹起一般不会遮挡它但若把它放进列表更深处就必须配合Scroll的自动滚动或keyboardAvoidMode来避让。好在 11 号把它放在首屏最显眼的位置规避了这套复杂度。六、QuickAction 四宫格快捷入口四宫格把寄件 / 取件 / 代取 / 查件四个最高频动作平铺成一行每个占layoutWeight(1)等宽。// QuickAction — 快捷入口 Builder QuickAction() { Row({ space: 10 }) { ForEach(this.quickActions, (item: QuickItem) { Column({ space: 6 }) { Text(item.emoji).fontSize(26) Text(item.label).fontSize(12).fontColor(C.textSub) } .layoutWeight(1).padding({ top: 14, bottom: 14 }) .backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) .onClick(() { promptAction.showToast({ message: item.label }); }) }, (item: QuickItem) item.label) }.width(100%) }每个入口由emoji 图标 文字标签组成图标用大字号 emoji 代替图片资源好处是零资源依赖、随时可换、不增加包体。四个入口整体不再套外层卡片而是各自独立描边视觉上更像四块可点的瓷砖。点击任意一块都弹对应标签的 Toast演示了ForEach中闭包捕获item的能力——注意箭头函数参数item就是当前遍历对象因此每个入口的点击事件天然绑定到自己的数据。用 emoji 当图标虽省事也有代价不同系统/字体下 emoji 的渲染尺寸和风格不一无法像 SVG/icon font 那样精确控制线宽与色相。在追求像素级统一的品牌项目里更稳妥的做法是把图标换成Image资源或矢量字体。11 号用 emoji 是为零资源 demo服务这个取舍在真实产品里要按需调整。七、WeekChart 本周取件趋势这是首页唯一的自绘图表。它用纯 ArkUI 组件叠出一组柱状图没有引入任何图表库。// WeekChart — 条形图 Builder WeekChart() { Column({ space: 12 }) { Row() { Text(本周取件趋势).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text) Blank() Text(峰值 95 件).fontSize(12).fontColor(C.textDim) }.width(100%) Row({ space: 6 }) { ForEach(this.weekBars, (w: WeekBar) { Column({ space: 6 }) { Column() { Column() .width(100%) .height(Math.floor(w.value / this.maxBar * 80)) .backgroundColor(C.primary).borderRadius(4) }.width(100%).height(80) Text(w.day).fontSize(11).fontColor(C.textDim) } .layoutWeight(1).alignItems(HorizontalAlign.Center) }, (w: WeekBar) w.day) }.width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rLg) .border({ width: 1, color: C.stroke }) }关键在于那行高度公式Math.floor(w.value / this.maxBar * 80)。它的含义是——把每天的数值w.value先除以最大值maxBar95归一化到 0~1再乘以图表区总高 80vp得到柱子实际高度。用Math.floor取整避免小数点带来的亚像素抖动。外层Column().height(80)固定了零线到底线的轨道高度内层柱子从顶部对齐alignItems(HorizontalAlign.Center) 内层 Column 默认顶端对齐向下生长——于是所有柱子底部天然对齐视觉上是一组规整的对比条形。用Math.floor取整避免小数点带来的亚像素抖动。外层Column().height(80)固定了零线到底线的轨道高度内层柱子从顶部对齐向下生长——于是所有柱子底部天然对齐视觉上是一组规整的对比条形。这种用纯布局比例堆出图表的手法在需求不复杂时非常划算零依赖、零额外组件、改数据即改图。但当你的图表需要动画过渡、点击 tooltip、或动态增删系列时就该考虑引入 ECharts 等图表库了。11 号首页刻意停留在纯声明式堆叠正是为了让读者先看清单图表的底层数学再决定要不要上重型方案——这是一个很有教学意义的取舍示范。八、ExpressList 快递动态列表快递动态是首页信息密度最高的区域。每条用ForEach渲染内部把图标 / 信息 / 时间状态三组信息横向排布并在底部嵌一条Progress进度条。// ExpressList — 快递列表 Builder ExpressList() { Column({ space: 10 }) { ForEach(this.expresses, (e: Express) { Column({ space: 10 }) { Row({ space: 12 }) { Row() { Text(e.emoji).fontSize(22) } .width(46).height(46).backgroundColor(C.primarySoft).borderRadius(D.rSm) .justifyContent(FlexAlign.Center) Column({ space: 4 }) { Row({ space: 8 }) { Text(e.company).fontSize(14).fontWeight(FontWeight.Medium).fontColor(C.text) Text(e.tag).fontSize(10) .fontColor(e.tag 待取件 ? C.warn : C.ok) .padding({ left: 6, right: 6, top: 2, bottom: 2 }) .backgroundColor(C.cardSoft).borderRadius(4) }.width(100%) Text(单号: e.code).fontSize(11).fontColor(C.textDim) Row({ space: 2 }) { ForEach([1, 2, 3, 4, 5], (n: number) Text(n e.rating ? ★ : ☆).fontSize(12) .fontColor(n e.rating ? C.warn : C.textDim), (n: number) n.toString()) }.width(100%) }.alignItems(HorizontalAlign.Start).layoutWeight(1) Column({ space: 4 }) { Text(e.time).fontSize(11).fontColor(C.textDim) Text(e.progress 100 ? 已完成 : 运输中).fontSize(10).fontColor(C.textDim) }.alignItems(HorizontalAlign.End) }.width(100%) Progress({ value: e.progress, total: 100 }).color(C.primary).width(100%) } .width(100%).padding(14) .backgroundColor(C.card).borderRadius(D.rMd) .border({ width: 1, color: C.stroke }) .onClick(() { promptAction.showToast({ message: e.company }); }) }, (e: Express) e.id.toString()) }.width(100%) }三个亮点其一是状态标签的颜色三元切换——e.tag 待取件 ? C.warn : C.ok待取件用警示黄、已取件用成功绿其二是星级评分用ForEach([1..5])直接生成n e.rating决定画实心星还是空心星纯声明式、零分支判断 UI其三是Progress组件——ArkUI 内置的进度条控件替代了手写Stack 宽度百分比的老办法一行Progress({ value, total:100 })就完成代码量骤减且效果一致。整张卡片可点点击弹公司名 Toast符合列表项即入口的习惯。ForEach([1,2,3,4,5])这种用固定数组造循环的写法在生成星级、步骤条、分页器时极常见。要注意它的 key 用了n.toString()因为 1~5 不重复能稳定标识。若循环生成的是可变数据务必换成数据自身的 id否则重绘时会出现星星错位。九、RankList 快递公司排行排行榜把本周各快递公司的取件量做了从高到低的排序展示。// RankList — 排行榜 Builder RankList() { Column({ space: 10 }) { ForEach(this.ranks, (r: RankItem) { Row({ space: 12 }) { Text(r.rank.toString()).fontSize(16).fontWeight(FontWeight.Bold) .fontColor(r.rank 1 ? C.warn : C.textDim).width(24) Text(r.emoji).fontSize(20) Text(r.name).fontSize(14).fontColor(C.text).layoutWeight(1) Text(r.value).fontSize(14).fontColor(C.primary).fontWeight(FontWeight.Medium) } .width(100%).padding({ left: 14, right: 14, top: 10, bottom: 10 }) .backgroundColor(C.card).borderRadius(D.rSm) .border({ width: 1, color: C.stroke }) .onClick(() { promptAction.showToast({ message: r.name }); }) }, (r: RankItem) r.id.toString()) }.width(100%) }序号r.rank用fontColor(r.rank 1 ? C.warn : C.textDim)做了条件着色——第一名橙色高亮、其余淡化制造冠军突出的视觉焦点。width(24)给序号固定宽度保证后面的 emoji 与名字纵向对齐整列看起来像一张规整的报表。数据上顺丰 8 件 / 圆通 5 件 / 京东 4 件三项展示了本周取件量排名前三的快递公司与首页 HeroCard 的本月取件 12等统计指标共同构成首页的数据全景。十、与系列其他应用首页的对比把 11 号首页和 10 号水电缴费首页放在一起看会发现同一个骨架两种性格两者都是Header Scroll(Column(...))的纵向滚动结构都用了StorageProp安全区与C/D双类主题。但 10 号更功能导向——首屏就是可交互的柱状图和操作卡片11 号更信息导向——首屏是渐变 HeroCard 动态列表把待取件提醒这种时效性内容推到最前面。差别背后是产品定位不同水电缴费是低频刚需每月一次用户进来的目的是马上操作快递是高频轻交互每天看状态用户进来的目的是扫一眼有没有新件。把高频信息前置、把操作收进快捷入口正是 11 号首页的设计巧思。更宏观地看11 号首页的渐变头部 卡片流范式在社交、电商、工具类应用里都通用顶部一块品牌渐变承载身份与关键指标下方用等宽卡片平铺功能与动态。你把这套骨架存进自己的组件库换套配色、换组数据源几乎能 10 分钟搭出一个新 App 的首页——这也是本系列用统一C/D双类主题的根本原因代码可以复制风格必须统一。十一、本章小结与工程经验组件核心技术学习价值HeaderStoragePropsafeTop沉浸式全屏安全区适配HeroCardlinearGradient 双按钮渐变头部 主次操作区分QuickActionborder 描边 RowForEach扁平化快捷入口WeekChartMath.floor映射高度纯展示型自绘条形图ExpressListProgress内置控件列表内嵌进度指示RankList条件字体色排名排行榜视觉层次沉浸式安全区适配是首页最值得迁移到任何项目的经验EntryAbility动态获取避让区 → 写入AppStorage→ 各页面用StorageProp读取这条链路让 UI 在不同机型上都能贴边而不越界。其次数据先抽象成 interface、UI 只负责渲染的写法让列表类页面天然具备可扩展性。最后能复用内置控件如Progress就别手搓既能减代码又能保证一致性。如果把首页拆成一条主线 七块积木主线是沉浸式安全区适配七块积木是 Header / HeroCard / SearchBar / QuickAction / WeekChart / ExpressList / RankList。主线决定了页面能不能贴边不越界积木决定了页面好不好看、信息密不密。两者解耦意味着你可以单独替换任意一块积木而不动主线——这正是组件化想要达到的高内聚、低耦合。下次写首页先画主线安全区 滚动骨架再往里填积木思路会清晰很多。
返回列表