从零开始构建HarmonyOS应用的首页导航界面

创建项目

由于前段时间没有申请到仓颉的鸿蒙6版本,所以目前本教程使用DevEco Studio 5.0.5 Release开发,对应HarmonyOS API17版本,搭配仓颉插件DevEco Studio-Cangjie Plugin 5.0.13.200 Canary。

新建项目的教程可以参考文章:从零开始创建你的第一个HarmonyOS6项目

只不过在新建项目的时候需要选择[Cangjie]Empty Ability:

Empty Ability

概览

在本文中,我们将深入讲解应用的主页实现。这是用户打开应用后看到的第一个界面,也是本应用的导航中心。通过本文学习,你将掌握:

  • 仓颉编程语言 struct 类型的定义和使用
  • UI 组件的基本用法(Column、Grid、Text 等)
  • 状态管理和组件装饰器
  • 列表渲染和事件处理
  • 无障碍设计的实践应用

完整代码展示

首先,让我们看一下完整的 index.cj 代码:

package ohos_app_cangjie_entry

import ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import std.collection.*

// 导航卡片数据结构
struct NavigationCard {
    public var icon: String
    public var title: String
    public var bgColor: Color      // 背景颜色
    
    public init(icon: String, title: String, bgColor: Color) {
        this.icon = icon
        this.title = title
        this.bgColor = bgColor
    }
}

// 主页入口视图
@Entry
@Component
class EntryView {
    // 8个功能卡片数据
    let cards: Array<NavigationCard> = [
        NavigationCard("药", "用药提醒", Color(0xFFB3BA)),      // 深粉红色
        NavigationCard("健", "健康日记", Color(0xBAE1B3)),      // 深绿色
        NavigationCard("急", "紧急联系", Color(0xFFCC99)),      // 深橙色
        NavigationCard("价", "价格记录", Color(0x99CCFF)),      // 深蓝色
        NavigationCard("乐", "娱乐", Color(0xFFB3E6)),          // 深粉色
        NavigationCard("电", "常用电话", Color(0x99E6E6)),      // 深青色
        NavigationCard("防", "防诈骗", Color(0xFFE699)),        // 深黄色
        NavigationCard("救", "应急指南", Color(0xD9B3FF))       // 深紫色
    ]
    
    func build(): Unit {
        Column() {
            // 标题栏
            Text("老年帮帮手")
                .fontSize(32)
                .fontWeight(FontWeight.Bold)
                .fontColor(0x333333)
                .margin(top: 20, bottom: 20)
            
            // 可滚动的卡片网格
            Scroll() {
                Grid() {
                    // 使用ForEach循环渲染卡片
                    ForEach(
                        this.cards,
                        itemGeneratorFunc: {card: NavigationCard, index: Int64 =>
                            GridItem() {
                                this.CardItem(card)
                            }
                        },
                        keyGeneratorFunc: {card: NavigationCard, index: Int64 =>
                            card.title
                        }
                    )
                }
                .columnsTemplate("1fr 1fr")
                .rowsGap(16)
                .columnsGap(16)
                .width(100.percent)
                .padding(16)
            }
            .width(100.percent)
            .layoutWeight(1)
        }
        .width(100.percent)
        .height(100.percent)
        .backgroundColor(0xF5F5F5)
    }
    
    // 卡片组件 - 使用@Builder装饰器
    @Builder
    func CardItem(card: NavigationCard): Unit {
        Column(12) {
            Text(card.icon)
                .fontSize(40)
                .fontColor(0x333333)
                .width(70)
                .height(70)
                .textAlign(TextAlign.Center)
                .backgroundColor(0xF0FFF0) 
                .borderRadius(40)
            
            // 标题
            Text(card.title)
                .fontSize(22)
                .fontWeight(FontWeight.Bold)
                .fontColor(0x333333)
        }
        .width(100.percent)
        .height(150)
        .padding(20)
        .backgroundColor(card.bgColor)
        .borderRadius(16)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .onClick {
            evt => AppLog.info("点击了: ${card.title}")
        }
    }
}

代码逐行解析

第一部分:包声明和导入

package ohos_app_cangjie_entry
  • 包声明定义了当前文件所属的命名空间

以下是导入语句:

import ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import std.collection.*

第二部分:数据模型定义

// 导航卡片数据结构
struct NavigationCard {
    public var icon: String
    public var title: String
    public var bgColor: Color      // 背景颜色
    
    public init(icon: String, title: String, bgColor: Color) {
        this.icon = icon
        this.title = title
        this.bgColor = bgColor
    }
}
什么是 struct?

在仓颉编程语言语言中,struct 是一种值类型的数据结构,用于组织相关的数据。它具有以下特点:

  1. 值语义:赋值时会进行拷贝,而不是共享引用
  2. 轻量级:适合表示简单的数据模型
  3. 不可继承:struct 不支持继承,但可以实现接口
struct vs class 对比
特性 struct class
类型 值类型 引用类型
赋值行为 拷贝 共享引用
继承 不支持 支持

为什么这里使用 struct?

NavigationCard 只是一个简单的数据容器,包含图标、标题和背景色三个字段。它不需要继承,也不需要复杂的行为,因此使用 struct 是最佳选择。

字段定义
public var icon: String
public var title: String
public var bgColor: Color
  • public:访问修饰符,表示这些字段可以从外部访问
  • var:表示可变变量(与 let 不可变变量相对)
  • String:字符串类型
  • Color:UI 框架提供的颜色类型
构造函数
public init(icon: String, title: String, bgColor: Color) {
    this.icon = icon
    this.title = title
    this.bgColor = bgColor
}

init 构造函数

  • init 是仓颉中的构造函数关键字
  • 参数列表定义了创建对象时需要传入的值
  • this 关键字指向当前对象实例
  • 构造函数中通常进行字段的初始化

使用示例

let card = NavigationCard("药", "用药提醒", Color(0xFFB3BA))

第三部分:主视图组件

@Entry
@Component
public class EntryView {
    // ...
}

装饰器

@Entry 装饰器
  • 标记这是入口组件
  • 应用启动时会自动加载并显示这个组件
@Component 装饰器
  • 标记这是一个UI 组件
  • 组件必须实现 build() 方法来定义 UI 结构
  • 组件可以包含状态、生命周期方法等

第四部分:数据初始化

let cards: Array<NavigationCard> = [
    NavigationCard("药", "用药提醒", Color(0xFFB3BA)),      // 深粉红色
    NavigationCard("健", "健康日记", Color(0xBAE1B3)),      // 深绿色
    NavigationCard("急", "紧急联系", Color(0xFFCC99)),      // 深橙色
    NavigationCard("价", "价格记录", Color(0x99CCFF)),      // 深蓝色
    NavigationCard("乐", "娱乐", Color(0xFFB3E6)),          // 深粉色
    NavigationCard("电", "常用电话", Color(0x99E6E6)),      // 深青色
    NavigationCard("防", "防诈骗", Color(0xFFE699)),        // 深黄色
    NavigationCard("救", "应急指南", Color(0xD9B3FF))       // 深紫色
]

知识点:数组字面量

  • Array<NavigationCard>:数组类型声明,元素类型为 NavigationCard
  • [...]:数组字面量语法,直接初始化数组内容
  • let:不可变变量,数组引用不能改变(但数组内容可以修改)

第五部分:build() 方法 - UI 构建

func build(): Unit {
    Column() {
        // UI 内容
    }
    .width(100.percent)
    .height(100.percent)
    .backgroundColor(0xF5F5F5)
}

知识点:build() 方法

  • 每个 @Component 必须实现 build() 方法
  • 返回类型是 Unit(相当于其他语言的 void)
  • 方法内部定义组件的 UI 结构

知识点:Column 组件

Column 是一个垂直布局容器,子元素从上到下排列。

┌─────────────────┐
│   子元素 1       │
├─────────────────┤
│   子元素 2       │
├─────────────────┤
│   子元素 3       │
└─────────────────┘

属性链式调用

.width(100.percent)        // 宽度 100%
.height(100.percent)       // 高度 100%
.backgroundColor(0xF5F5F5) // 背景色(浅灰色)
  • UI 组件支持链式调用
  • 每个属性方法返回组件自身,可以连续调用
  • 100.percent 是百分比单位,表示占满父容器

第六部分:标题栏

Text("老年帮帮手")
    .fontSize(32)
    .fontWeight(FontWeight.Bold)
    .fontColor(0x333333)
    .margin(top: 20, bottom: 20)

知识点:Text 组件

Text 是显示文本的基础组件。

属性详解

属性 说明
fontSize 32 字体大小 32sp
fontWeight FontWeight.Bold 字体粗细(加粗)
fontColor 0x333333 字体颜色(深灰色)
margin top: 20, bottom: 20 上下外边距各 20dp

第七部分:可滚动网格布局

Scroll() {
    Grid() {
        // 网格内容
    }
    .columnsTemplate("1fr 1fr")
    .rowsGap(16)
    .columnsGap(16)
    .width(100.percent)
    .padding(16)
}
.width(100.percent)
.layoutWeight(1)

知识点:Scroll 组件

Scroll 是一个可滚动容器,当内容超出屏幕时自动显示滚动条。

为什么需要 Scroll?

  • 8 个卡片可能超出屏幕高度
  • 提供良好的用户体验

知识点:Grid 组件

Grid 是一个网格布局容器,可以将子元素排列成行和列。

┌──────────┬──────────┐
│  卡片1   │  卡片2   │
├──────────┼──────────┤
│  卡片3   │  卡片4   │
├──────────┼──────────┤
│  卡片5   │  卡片6   │
└──────────┴──────────┘

Grid 属性详解

1. columnsTemplate(“1fr 1fr”)

定义网格的列模板。

  • 1fr 1fr:两列,每列占 1 份空间(平分)
  • fr 是 fraction(分数)单位
  • 相当于 CSS Grid 的 grid-template-columns

其他示例

"1fr 2fr"      // 两列,第二列是第一列的 2 倍宽
"1fr 1fr 1fr"  // 三列,平分
2. rowsGap(16) 和 columnsGap(16)
  • rowsGap:行间距 16dp
  • columnsGap:列间距 16dp

视觉效果

┌────┐ 16dp ┌────┐
│卡片│<---->│卡片│
└────┘      └────┘
  ↕ 16dp
┌────┐      ┌────┐
│卡片│      │卡片│
└────┘      └────┘
3. layoutWeight(1)
  • 设置组件在父容器中的权重
  • 1 表示占据剩余的所有空间
  • 确保网格区域填满标题栏下方的所有空间

布局结构

┌─────────────────────┐
│   标题栏(固定高度)  │  ← 固定大小
├─────────────────────┤
│                     │
│   网格区域           │  ← layoutWeight(1),占满剩余空间
│   (可滚动)          │
│                     │
└─────────────────────┘

第八部分:ForEach 列表渲染

ForEach(
   this.cards,
    itemGeneratorFunc: {card: NavigationCard, index: Int64 =>
        GridItem() {
            this.CardItem(card)
        }
    },
    keyGeneratorFunc: {card: NavigationCard, index: Int64 =>
        card.title
    }
)

知识点:ForEach 组件

ForEach 是一个列表渲染组件,用于根据数据数组动态生成 UI 元素。

参数详解

1. 数据源:this.cards
  • 要遍历的数组
  • this 指向当前组件实例
2. itemGeneratorFunc(项生成函数)
{card: NavigationCard, index: Int64 =>
    GridItem() {
        this.CardItem(card)
    }
}

这是一个 lambda 表达式(匿名函数):

  • card:当前遍历到的数组元素
  • index:当前元素的索引(0, 1, 2, …)
  • =>:lambda 箭头,分隔参数和函数体
  • 返回值:一个 GridItem 组件

Lambda 语法回顾

// 完整形式
{参数1: 类型1, 参数2: 类型2 => 函数体}

// 示例
{x: Int64, y: Int64 => x + y}  // 加法函数
{name: String => println(name)} // 打印函数
3. keyGeneratorFunc(键生成函数)
{card: NavigationCard, index: Int64 =>
    card.title
}
  • 为每个列表项生成唯一的键(key)
  • 这里使用 card.title 作为键(因为每个卡片标题都是唯一的)

知识点:GridItem 组件

GridItem 是 Grid 的子项容器,每个 GridItem 占据网格中的一个单元格。

GridItem() {
    this.CardItem(card)
}
  • 包裹单个卡片组件
  • 自动适应网格布局

第九部分:CardItem 卡片组件

@Builder
func CardItem(card: NavigationCard): Unit {
    Column(12) {
        // 卡片内容
    }
    .width(100.percent)
    .height(160)
    .padding(20)
    .backgroundColor(card.bgColor)
    .borderRadius(16)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .onClick {
        evt => AppLog.info("点击了: ${card.title}")
    }
}

知识点:@Builder 装饰器

  • 标记这是一个可复用的 UI 构建函数
  • 可以在 build() 方法中多次调用
  • 类似于 React 的函数组件

为什么使用 @Builder?

  1. 代码复用:避免重复编写相同的 UI 代码
  2. 逻辑分离:将卡片的渲染逻辑独立出来
  3. 可维护性:修改卡片样式只需改一处
Column(12) - 间距参数
Column(12) {
    // 子元素
}
  • 12 是子元素之间的默认间距(12dp)
  • 相当于给每个子元素之间添加 margin

视觉效果

┌─────────────┐
│   图标      │
│             │  ← 12dp 间距
│   标题      │
└─────────────┘
卡片样式属性

让我们逐个解析卡片的样式属性:

属性 说明
width 100.percent 宽度占满网格单元格
height 160 固定高度 160
padding 20 内边距 20
backgroundColor card.bgColor 使用卡片数据中的背景色
borderRadius 16 圆角半径 16(圆润的卡片)
justifyContent FlexAlign.Center 垂直方向居中对齐
alignItems HorizontalAlign.Center 水平方向居中对齐

视觉效果

┌──────────────────────┐
│                      │ ← padding: 20
│      ┌────┐          │
│      │图标│          │ ← 居中对齐
│      └────┘          │
│       标题           │
│                      │
└──────────────────────┘
       ↑
   borderRadius: 16
   (圆角)

第十部分:图标显示

Text(card.icon)
    .fontSize(40)
    .fontColor(0x333333)
    .width(80)
    .height(80)
    .textAlign(TextAlign.Center)
    .backgroundColor(0xFFFFFF)
    .borderRadius(40)

设计思路

这里使用文字作为图标,而不是图片:

  1. 简单直观:汉字图标对老年人更友好(本应用的面向人群是老年人)
  2. 无需资源:不需要准备图标图片
  3. 易于修改:直接改文字即可

圆形图标实现

.width(80)
.height(80)
.borderRadius(40)  // 半径 = 宽高的一半,形成正圆

属性详解

  • fontSize(40):大字体,清晰可见
  • fontColor(0x333333):深灰色,与背景形成对比
  • textAlign(TextAlign.Center):文字居中
  • backgroundColor(0xFFFFFF):白色背景
  • borderRadius(40):圆形(半径 = 宽高的一半)

第十一部分:标题显示

Text(card.title)
    .fontSize(22)
    .fontWeight(FontWeight.Bold)
    .fontColor(0x333333)

第十二部分:点击事件处理

.onClick {
    evt => AppLog.info("点击了: ${card.title}")
}

知识点:事件处理

onClick 事件
  • onClick 是点击事件处理器
  • 接受一个 lambda 函数作为参数
  • evt 是事件对象(这里未使用)
AppLog.info() 日志输出
AppLog.info("点击了: ${card.title}")
  • info() 输出信息级别的日志
  • ${card.title}字符串插值语法

知识点:字符串插值

"点击了: ${card.title}"
  • ${} 内可以放置任何表达式
  • 表达式的值会被转换为字符串并插入
  • 类似于 JavaScript 的模板字符串

示例

let name = "张三"
let age = 65
let message = "姓名: ${name}, 年龄: ${age}"
// 结果: "姓名: 张三, 年龄: 65"

界面效果展示

最终界面布局

效果


本章总结

通过本章学习,我们完成了老年帮帮手应用的主页实现。让我们回顾一下关键要点:

核心概念

  1. 数据模型:使用 struct 定义 NavigationCard
  2. 组件化:使用 @Component 和 @Builder 构建可复用组件
  3. 布局系统:掌握 Column、Grid、Scroll 的使用
  4. 列表渲染:使用 ForEach 动态生成 UI
  5. 事件处理:实现 onClick 点击事件
  6. 无障碍设计:大字体、高对比度、大触摸目标

如果大家有任何疑问,欢迎在下方评论区留言~

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐