【鸿蒙原生应用开发实战】第二篇:首页开发——宠物卡片+快捷入口+动态信息流

【鸿蒙原生应用开发实战】第二篇:首页开发------宠物卡片+快捷入口+动态信息流

上一篇我们搭好了项目架子,这一篇开始写真实代码。首页是一个 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 页签切换(健康档案/体重记录/疫苗记录)
  • 柱状图式体重趋势展示

敬请期待!🚀


系列导航:

  • 第一篇:项目搭建与架构
  • 第二篇:首页开发(本文)
  • 第三篇:表单与详情页
  • 第四篇:相册与提醒功能
  • 第五篇:总结与最佳实践
相关推荐
枫叶丹41 小时前
【HarmonyOS 6.0】MDM Kit 深度解析:企业级 user_grant 权限集中管理策略
开发语言·华为·harmonyos
风华圆舞2 小时前
鸿蒙 + Flutter 下如何管理 AI 会话——AgentService 设计解析
人工智能·flutter·harmonyos
川石课堂软件测试2 小时前
UI自动化测试|下拉选择框&弹出框&滚动条操作实践
开发语言·python·jmeter·ui·docker·单元测试·harmonyos
枫叶丹42 小时前
【HarmonyOS 6.0】MDM Kit 新增支持通过设备管理设置桌面壁纸能力详解
华为·harmonyos
狼哥16862 小时前
《新闻资讯》九、应用各分层模块实现指南
ui·华为·harmonyos
小雨下雨的雨3 小时前
HarmonyOS V2状态管理深度解析:列表数据与分页架构
华为·架构·harmonyos·鸿蒙
坚果派·白晓明12 小时前
【鸿蒙PC】SDL3 适配:AtomCode + Skills 快速集成 NAPI 测试工具
c++·华为·ai编程·harmonyos·atomcode
YM52e14 小时前
男孩子在外自我保护指南——用鸿蒙 ArkTS 构建交互式安全教育应用
学习·安全·华为·harmonyos·鸿蒙·鸿蒙系统