【鸿蒙原生应用开发实战】第二篇:首页开发——宠物卡片+快捷入口+动态信息流
【鸿蒙原生应用开发实战】第二篇:首页开发——宠物卡片+快捷入口+动态信息流
上一篇我们搭好了项目架子,这一篇开始写真实代码。首页是一个 App 的门面,我们的"萌宠日记"首页包含了宠物切换、快速入口、动态信息流三大模块,麻雀虽小五脏俱全。本文手把手带你实现全部 UI,并讲解 ArkTS 的核心语法。
一、首页整体布局
先看页面结构:
Index.ets ├── 顶部标题栏 (buildHeader) ← "🐾 萌宠日记" + 通知按钮 ├── 宠物卡片区 (buildPetCards) ← 横向滚动 + 选中卡片信息面板 ├── 快捷操作栏 (buildQuickActions) ← 添加宠物/相册/提醒/成长记录 └── 萌宠动态 (buildMoments) ← 动态列表 Feeds每个模块都是一个@Builder装饰的方法,在build()中按顺序组装。这种组件化拆分的手段,是管理 ArkTS 复杂页面的核心技巧。
二、数据类型定义
在 Index.ets 顶部,我们定义了两个接口:
interfacePet{id:number;name:string;type:string;// 表情符号,如 '🐱'breed:string;// 品种,如 '英短蓝猫'age:string;// 年龄,如 '2岁3个月'avatar:string;weight:string;// 体重,如 '4.5kg'vaccineDate:string;// 最近疫苗日期dewormDate:string;// 最近驱虫日期}interfaceMoment{id:number;petName:string;// 关联宠物名content:string;// 动态内容time:string;// 发布时间描述likes:number;// 点赞数imageCount:number;// 配图数量}在 ArkTS 严格模式下,所有对象字面量必须有显式类型声明(
arkts-no-untyped-obj-literals规则)。所以我们在initPets()中初始化数据时,this.pets = [...]的数组元素必须符合Pet接口。
三、问题:首页开发——宠物卡片横向滚动
3.1 宠物头像选择器
这是首页最核心的交互组件——用户通过横向滑动选择宠物,选中后下方显示该宠物的详细信息:
@BuilderbuildPetCards(){Column(){Scroll(){Row(){ForEach(this.pets,(pet:Pet,index?:number)=>{Column(){Stack(){Column().width(68).height(68).borderRadius(34).backgroundColor(this.selectedPetIndex===(indexasnumber)?'#FF6B35':'#F0F0F0')Text(pet.type).fontSize(30)// 显示 🐱/🐶/🐰}.width(68).height(68)Text(pet.name).fontSize(13).fontWeight(FontWeight.Medium).fontColor('#333333')Text(pet.breed).fontSize(10).fontColor('#999999')}.margin({right:16}).onClick(()=>{this.selectedPetIndex=indexasnumber;})},(pet:Pet)=>pet.id.toString())}.padding({left:16})}.scrollable(ScrollDirection.Horizontal).height(120)// ... 下方信息面板}}关键知识点:
Scroll+ScrollDirection.Horizontal
实现横向滚动的标准组合。Scroll包裹Row,设置scrollable(ScrollDirection.Horizontal),当内容超出屏幕宽度时自动可滑动。
Stack层叠布局
Stack用于将头像圆形背景和 Emoji 叠加在一起。这比用Row+Column更简洁,且支持绝对定位。
选中状态切换
通过selectedPetIndex状态变量和三元表达式动态切换背景色:
.backgroundColor(this.selectedPetIndex===index?'#FF6B35':'#F0F0F0')3.2 选中宠物的信息面板
点击宠物头像后,下方显示三列信息 + 详情入口:
Row(){Column(){Text(this.pets[this.selectedPetIndex].weight).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#FF6B35')Text('体重').fontSize(10).fontColor('#999999')}.layoutWeight(1).alignItems(HorizontalAlign.Center)Column(){Text(this.pets[this.selectedPetIndex].age).fontSize(16).fontWeight(FontWeight.Bold).fontColor('#3498DB')Text('年龄')}.layoutWeight(1).alignItems(HorizontalAlign.Center)// ... 品种列Column(){Text('详情>').fontSize(12).fontColor('#FF6B35')}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick(()=>{router.pushUrl({url:'pages/PetDetailPage',params:{petId:this.pets[this.selectedPetIndex].id}});})}.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(10)知识点:layoutWeight(1)实现等分
layoutWeight(1)让四个 Column 平分 Row 的宽度,比写死百分比更灵活。这是 ArkUI 中实现等分布局的推荐做法。
四、快捷操作栏
四个功能入口整齐排列:
@BuilderbuildQuickActions(){Row(){Column(){Text('➕').fontSize(22)Text('添加宠物').fontSize(11).fontColor('#666666')}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick(()=>router.pushUrl({url:'pages/AddPetPage'}))Column(){Text('📷').fontSize(22)Text('相册').fontSize(11).fontColor('#666666')}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick(()=>router.pushUrl({url:'pages/AlbumPage'}))Column(){Text('💉').fontSize(22)Text('提醒').fontSize(11).fontColor('#666666')}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick(()=>router.pushUrl({url:'pages/ReminderPage'}))Column(){Text('📊').fontSize(22)Text('成长记录').fontSize(11).fontColor('#666666')}.layoutWeight(1).alignItems(HorizontalAlign.Center).onClick(()=>router.pushUrl({url:'pages/PetDetailPage',params:{petId:...}}))}.width('100%').padding(14).backgroundColor('#FFFFFF').margin({top:12,left:16,right:16}).borderRadius(12)}这里有一个有意思的设计细节:四个入口中,"添加宠物"和"成长记录"跳转到不同页面,但"成长记录"复用了PetDetailPage(通过 params 传参区分)。这种页面复用的策略可以减少页面数量,保持路由简洁。
五、萌宠动态信息流
5.1 动态列表渲染
@BuilderbuildMoments(){Column(){Row(){Text('📝 萌宠动态').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#1A1A2E')Blank()Text(`共${this.moments.length}条`).fontSize(12).fontColor('#999999')}.width('100%').padding({left:16,right:16,top:16})ForEach(this.moments,(moment:Moment)=>{Column(){// 头部:头像 + 名字 + 时间Row(){Stack(){Column().width(36).height(36).borderRadius(18).backgroundColor('#FFE0D0')Text(moment.petName==='团子'?'🐱':moment.petName==='豆豆'?'🐶':'🐰').fontSize(18)}Column(){Text(moment.petName).fontSize(14).fontWeight(FontWeight.Medium)Text(moment.time).fontSize(10).fontColor('#BBBBBB')}.margin({left:8}).layoutWeight(1)Text('···').fontSize(14).fontColor('#CCCCCC')}.width('100%')// 正文Text(moment.content).fontSize(13).fontColor('#666666').lineHeight(20).width('100%').margin({top:8})// 配图占位(有图才显示)if(moment.imageCount>0){Row(){ForEach([1,2,3],(i:number)=>{if(i<=moment.imageCount){Column().width(80).height(80).backgroundColor('#F0F0F0').borderRadius(6).margin({right:6})}},(i:number)=>i.toString())}.width('100%').margin({top:6}).height(80)}// 底部互动:点赞 + 评论Row(){Blank()Text(`❤️${moment.likes}`).fontSize(12).fontColor('#999999').margin({left:12})Text('💬 评论').fontSize(12).fontColor('#999999').margin({left:12})}.width('100%').margin({top:6})}.width('100%').padding(14).backgroundColor('#FFFFFF').borderRadius(10).margin({top:8,left:16,right:16}).alignItems(HorizontalAlign.Start)},(moment:Moment)=>moment.id.toString())}.width('100%')}5.2 ForEach 的关键参数
ForEach的第三个参数是键值生成器,用于追踪列表项的增删改:
ForEach(this.moments,// 数据源(moment:Moment)=>{...},// UI 模板(moment:Moment)=>moment.id.toString()// key 生成器)为什么要传 key?
- 提升 diff 更新性能
- 避免列表项重排时 UI 闪烁
- 符合 ArkTS 的渲染优化要求
5.3 条件渲染的优雅写法
配图区域只在moment.imageCount > 0时才渲染:
if(moment.imageCount>0){Row(){/* 图片占位 */}}这是 ArkTS 中条件渲染的标准写法。不需要v-if或*ngIf,直接用if语句。
六、@State 装饰器与响应式编程
整个首页的核心状态只有三个变量:
@Statepets:Pet[]=[];// 宠物列表@Statemoments:Moment[]=[];// 动态列表@StateselectedPetIndex:number=0;// 选中的宠物索引@State是 ArkTS 响应式系统的基石:
| 特性 | 说明 |
|---|---|
| 自动追踪 | @State变量被读取时自动记录依赖 |
| 精准更新 | 变量变化时只重新渲染依赖它的组件 |
| 不可变替换 | 数组/对象需要整体替换而非修改属性 |
注意:
@State只能装饰属于组件自身的状态,如果状态需要跨组件共享,需要使用@Prop、@Link或@Provide/@Consume。
七、UI 设计技巧总结
7.1 颜色系统
我们定义了一套隐式的色彩规范:
| 用途 | 色值 | 使用场景 |
|---|---|---|
| 主色调 | #FF6B35 | 选中状态、按钮、链接 |
| 深色文字 | #333333 | 标题、正文 |
| 灰色文字 | #999999 | 辅助信息、说明 |
| 浅灰文字 | #BBBBBB | 时间戳、次要信息 |
| 页面背景 | #F5F5F5 | 整体背景色 |
| 卡片背景 | #FFFFFF | 所有卡片 |
7.2 卡片阴影与圆角
所有卡片统一风格:
.backgroundColor('#FFFFFF').borderRadius(10)// 圆角.margin({top:8,left:16,right:16})虽然没有显式加阴影,但白色卡片在浅灰背景上已经产生视觉层次感。需要阴影时可以用.shadow()方法。
7.3 Emoji 的妙用
全篇大量使用了 Emoji 作为图标:
| 场景 | Emoji |
|---|---|
| 宠物类型 | 🐱 🐶 🐰 |
| 快捷入口 | ➕ 📷 💉 📊 |
| 动态互动 | ❤️ 💬 |
| 通知按钮 | 🔔 |
这样做的好处是零资源开销——不需要引入任何图标库或图片资源,一个 Unicode 字符搞定。对小型应用来说,这是性价比最高的 UI 方案。
八、完整 build() 组装
build():void{Column(){this.buildHeader()// 顶部标题栏Scroll(){Column(){this.buildPetCards()// 宠物卡片this.buildQuickActions()// 快捷操作this.buildMoments()// 动态信息流}.width('100%').padding({bottom:20})}.scrollable(ScrollDirection.Vertical).layoutWeight(1).width('100%')}.width('100%').height('100%').backgroundColor('#F5F5F5')}整个页面被包裹在一个Column中,其中Scroll占满剩余空间(layoutWeight(1)),内部再嵌套Column按顺序排列三个模块。
这种垂直滚动 + 横向子滚动的嵌套结构,是移动端首页最经典的布局模式。
九、知识点串讲
| 知识点 | 本页示例 |
|---|---|
@Builder方法拆分 | 4个 Builder 分别对应4个 UI 区块 |
@State响应式变量 | pets, moments, selectedPetIndex |
Scroll双向滚动 | 纵向整页 + 横向宠物卡片 |
ForEach列表渲染 | 宠物列表 + 动态列表 |
Stack层叠布局 | 圆形头像背景 + Emoji |
layoutWeight等分 | 快捷入口四等分 |
if条件渲染 | 配图区域的显示/隐藏 |
router.pushUrl | 跳转到详情/添加/相册/提醒页 |
十、下篇预告
下一篇我们做添加宠物页面 + 宠物详情页:
- TextInput 表单的完整实现
- 标签选择器(宠物类型/性别)
- 三个 Tab 页签切换(健康档案/体重记录/疫苗记录)
- 柱状图式体重趋势展示
敬请期待!🚀
系列导航:
- 第一篇:项目搭建与架构
- 第二篇:首页开发(本文)
- 第三篇:表单与详情页
- 第四篇:相册与提醒功能
- 第五篇:总结与最佳实践
