【鸿蒙原生应用开发实战】第二篇:首页开发------宠物卡片+快捷入口+动态信息流
上一篇我们搭好了项目架子,这一篇开始写真实代码。首页是一个 App 的门面,我们的"萌宠日记"首页包含了宠物切换、快速入口、动态信息流三大模块,麻雀虽小五脏俱全。本文手把手带你实现全部 UI,并讲解 ArkTS 的核心语法。
一、首页整体布局
先看页面结构:
Index.ets
├── 顶部标题栏 (buildHeader) ← "🐾 萌宠日记" + 通知按钮
├── 宠物卡片区 (buildPetCards) ← 横向滚动 + 选中卡片信息面板
├── 快捷操作栏 (buildQuickActions) ← 添加宠物/相册/提醒/成长记录
└── 萌宠动态 (buildMoments) ← 动态列表 Feeds
每个模块都是一个 @Builder 装饰的方法,在 build() 中按顺序组装。这种组件化拆分的手段,是管理 ArkTS 复杂页面的核心技巧。
二、数据类型定义
在 Index.ets 顶部,我们定义了两个接口:
typescript
interface Pet {
id: number;
name: string;
type: string; // 表情符号,如 '🐱'
breed: string; // 品种,如 '英短蓝猫'
age: string; // 年龄,如 '2岁3个月'
avatar: string;
weight: string; // 体重,如 '4.5kg'
vaccineDate: string; // 最近疫苗日期
dewormDate: string; // 最近驱虫日期
}
interface Moment {
id: number;
petName: string; // 关联宠物名
content: string; // 动态内容
time: string; // 发布时间描述
likes: number; // 点赞数
imageCount: number; // 配图数量
}
在 ArkTS 严格模式下,所有对象字面量必须有显式类型声明 (
arkts-no-untyped-obj-literals规则)。所以我们在initPets()中初始化数据时,this.pets = [...]的数组元素必须符合Pet接口。
三、问题:首页开发------宠物卡片横向滚动
3.1 宠物头像选择器
这是首页最核心的交互组件------用户通过横向滑动选择宠物,选中后下方显示该宠物的详细信息:
typescript
@Builder buildPetCards() {
Column() {
Scroll() {
Row() {
ForEach(this.pets, (pet: Pet, index?: number) => {
Column() {
Stack() {
Column()
.width(68).height(68).borderRadius(34)
.backgroundColor(this.selectedPetIndex === (index as number) ? '#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 = index as number;
})
}, (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 状态变量和三元表达式动态切换背景色:
typescript
.backgroundColor(this.selectedPetIndex === index ? '#FF6B35' : '#F0F0F0')
3.2 选中宠物的信息面板
点击宠物头像后,下方显示三列信息 + 详情入口:
typescript
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 中实现等分布局的推荐做法。
四、快捷操作栏
四个功能入口整齐排列:
typescript
@Builder buildQuickActions() {
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 动态列表渲染
typescript
@Builder buildMoments() {
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 的第三个参数是键值生成器,用于追踪列表项的增删改:
typescript
ForEach(
this.moments, // 数据源
(moment: Moment) => { ... }, // UI 模板
(moment: Moment) => moment.id.toString() // key 生成器
)
为什么要传 key?
- 提升 diff 更新性能
- 避免列表项重排时 UI 闪烁
- 符合 ArkTS 的渲染优化要求
5.3 条件渲染的优雅写法
配图区域只在 moment.imageCount > 0 时才渲染:
typescript
if (moment.imageCount > 0) {
Row() { /* 图片占位 */ }
}
这是 ArkTS 中条件渲染的标准写法。不需要 v-if 或 *ngIf,直接用 if 语句。
六、@State 装饰器与响应式编程
整个首页的核心状态只有三个变量:
typescript
@State pets: Pet[] = []; // 宠物列表
@State moments: Moment[] = []; // 动态列表
@State selectedPetIndex: number = 0; // 选中的宠物索引
@State 是 ArkTS 响应式系统的基石:
| 特性 | 说明 |
|---|---|
| 自动追踪 | @State 变量被读取时自动记录依赖 |
| 精准更新 | 变量变化时只重新渲染依赖它的组件 |
| 不可变替换 | 数组/对象需要整体替换而非修改属性 |
注意:
@State只能装饰属于组件自身的状态 ,如果状态需要跨组件共享,需要使用@Prop、@Link或@Provide/@Consume。
七、UI 设计技巧总结
7.1 颜色系统
我们定义了一套隐式的色彩规范:
| 用途 | 色值 | 使用场景 |
|---|---|---|
| 主色调 | #FF6B35 |
选中状态、按钮、链接 |
| 深色文字 | #333333 |
标题、正文 |
| 灰色文字 | #999999 |
辅助信息、说明 |
| 浅灰文字 | #BBBBBB |
时间戳、次要信息 |
| 页面背景 | #F5F5F5 |
整体背景色 |
| 卡片背景 | #FFFFFF |
所有卡片 |
7.2 卡片阴影与圆角
所有卡片统一风格:
typescript
.backgroundColor('#FFFFFF')
.borderRadius(10) // 圆角
.margin({ top: 8, left: 16, right: 16 })
虽然没有显式加阴影,但白色卡片在浅灰背景上已经产生视觉层次感。需要阴影时可以用 .shadow() 方法。
7.3 Emoji 的妙用
全篇大量使用了 Emoji 作为图标:
| 场景 | Emoji |
|---|---|
| 宠物类型 | 🐱 🐶 🐰 |
| 快捷入口 | ➕ 📷 💉 📊 |
| 动态互动 | ❤️ 💬 |
| 通知按钮 | 🔔 |
这样做的好处是零资源开销------不需要引入任何图标库或图片资源,一个 Unicode 字符搞定。对小型应用来说,这是性价比最高的 UI 方案。
八、完整 build() 组装
typescript
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 页签切换(健康档案/体重记录/疫苗记录)
- 柱状图式体重趋势展示
敬请期待!🚀

系列导航:
- 第一篇:项目搭建与架构
- 第二篇:首页开发(本文)
- 第三篇:表单与详情页
- 第四篇:相册与提醒功能
- 第五篇:总结与最佳实践