📖 前言

背单词是很多人的刚需,而单词卡片(Flashcard)是一种经典且高效的学习方式。今天,我将带大家使用鸿蒙ArkUI从零开发一款单词记忆卡App。这个项目虽然功能看起来简单,但涉及到了状态管理、条件渲染、进度条可视化、动态UI切换等核心知识点,非常适合初学者练手。

先看最终效果:

在这里插入图片描述


一、项目概述

1.1 功能需求

这款单词记忆卡App具备以下核心功能:

功能模块详细说明
单词展示显示当前单词,点击卡片翻转显示释义
卡片翻转正面显示单词,背面显示释义和例句
学习进度顶部进度条实时显示学习进度
上下切换支持上一个/下一个单词切换
认识/不认识翻转后可标记掌握情况
单词计数实时显示当前进度(如 5/30)

1.2 技术亮点

  • 🔄 卡片翻转效果 - 点击卡片切换正反面内容
  • 📊 进度条可视化 - 动态计算并显示学习进度
  • 🎨 暗色主题设计 - 黑色背景,现代简洁风格
  • 🔀 动态按钮切换 - 根据卡片状态显示不同操作按钮
  • 📚 内置词库 - 30个精选英语高频词汇

1.3 技术栈

技术说明
开发框架ArkUI(声明式UI)
开发语言ArkTS(TypeScript扩展)
API版本23
开发工具DevEco Studio
项目模型Stage模型

二、项目创建与环境配置

2.1 创建新项目

  1. 打开 DevEco Studio,选择 Create Project
  2. 选择 Empty Ability 模板
  3. 填写项目信息:
    • Project name: WordCardApp
    • Bundle name: com.example.wordcardapp
    • Save location: E:\HMproject\Project\WordCardApp
    • Compile SDK: 选择最新的 HarmonyOS Next SDK
    • Model: Stage

2.2 项目结构

WordCardApp/
├── AppScope/
│   ├── app.json5                    # 应用全局配置
│   └── resources/                   # 全局资源
├── entry/
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets    # 应用入口
│   │   │   └── pages/
│   │   │       └── Index.ets            # 主页面(核心)
│   │   └── resources/
│   │       ├── base/element/            # 字符串、颜色
│   │       ├── base/media/              # 图片资源
│   │       └── base/profile/            # 页面路由
│   ├── module.json5                  # 模块配置
│   └── build-profile.json5           # 构建配置
└── oh_modules/                       # 依赖模块

三、数据模型设计

3.1 单词数据结构

定义单词卡片的接口:

interface WordItem {
  word: string      // 单词
  meaning: string   // 释义
  example: string   // 例句
}

3.2 内置词库

本App内置30个精选英语高频词汇:

private readonly WORDS: WordItem[] = [
  { word: 'abandon', meaning: 'v. 放弃;遗弃', example: 'They had to abandon the project.' },
  { word: 'benefit', meaning: 'n. 好处 v. 受益', example: 'Exercise benefits your health.' },
  { word: 'challenge', meaning: 'n. 挑战 v. 质疑', example: 'This is a big challenge.' },
  { word: 'dedicate', meaning: 'v. 致力于;献身', example: 'She dedicated her life to science.' },
  { word: 'efficient', meaning: 'adj. 高效的', example: 'This method is very efficient.' },
  { word: 'flexible', meaning: 'adj. 灵活的', example: 'We need a flexible schedule.' },
  { word: 'generate', meaning: 'v. 产生;发电', example: 'The wind generates electricity.' },
  { word: 'highlight', meaning: 'v. 强调 n. 亮点', example: 'The report highlights key issues.' },
  { word: 'influence', meaning: 'n./v. 影响', example: 'Media influences public opinion.' },
  { word: 'maintain', meaning: 'v. 维持;保养', example: 'Exercise helps maintain health.' },
  // ... 共30个单词
]

词库设计原则:

  1. 高频实用 - 选取日常和考试中常见的词汇
  2. 信息完整 - 包含词性、释义、例句三要素
  3. 例句真实 - 例句简洁实用,便于理解记忆

四、状态管理设计

4.1 状态变量定义

单词卡App的状态管理相对简洁:

@Entry
@Component
struct Index {
  // 当前单词索引
  @State currentIndex: number = 0
  
  // 卡片翻转状态
  @State showMeaning: boolean = false
}

4.2 状态变量说明

状态变量类型初始值作用
currentIndexnumber0当前显示的单词索引
showMeaningbooleanfalse是否显示释义(卡片翻转状态)

4.3 计算属性:进度百分比

get progress(): number {
  return Math.floor(((this.currentIndex + 1) / this.WORDS.length) * 100)
}

这个计算属性会根据当前索引自动计算学习进度百分比。


五、核心功能实现

5.1 卡片翻转逻辑

点击卡片切换正反面:

private toggleCard(): void {
  this.showMeaning = !this.showMeaning
}

5.2 下一个单词

private nextCard(): void {
  if (this.currentIndex + 1 < this.WORDS.length) {
    this.currentIndex++
    this.showMeaning = false  // 重置为正面
  }
}

注意: 切换单词时要重置 showMeaningfalse,确保新单词从正面开始显示。

5.3 上一个单词

private prevCard(): void {
  if (this.currentIndex > 0) {
    this.currentIndex--
    this.showMeaning = false
  }
}

5.4 边界检查

在切换时进行边界检查,防止数组越界:

  • nextCard():检查 currentIndex + 1 < WORDS.length
  • prevCard():检查 currentIndex > 0

六、UI布局实现

6.1 整体结构

build() {
  Column() {
    // 1. 顶部标题栏 + 进度计数
    Row() { /* ... */ }
    
    // 2. 进度条
    Row() { /* ... */ }
    
    // 3. 单词卡片
    Column() {
      if (!this.showMeaning) {
        // 正面:单词
      } else {
        // 背面:释义 + 例句
      }
    }
    .onClick(() => this.toggleCard())
    
    // 4. 操作按钮
    if (!this.showMeaning) {
      // 上一个/下一个
    } else {
      // 不认识/认识
    }
  }
}

6.2 顶部标题栏

Row() {
  Text('📚 单词卡')
    .fontSize(22)
    .fontWeight(FontWeight.Bold)
    .fontColor(Color.White)
  Blank()
  Text(String(this.currentIndex + 1) + ' / ' + String(this.WORDS.length))
    .fontSize(14)
    .fontColor('#8E8E93')
}
.width('100%')
.padding(16)

效果说明:

  • 左侧:应用标题 + emoji图标
  • 右侧:当前进度(如 “5 / 30”)

6.3 进度条实现

Row() {
  Column()
    .width(String(this.progress) + '%')  // 动态宽度
    .height(4)
    .borderRadius(2)
    .backgroundColor('#FF9F0A')  // 橙色进度
}
.width('100%')
.height(4)
.backgroundColor('#2C2C2E')  // 灰色背景
.padding({ left: 16, right: 16 })

进度条原理:

  1. 外层 Row 作为进度条背景(灰色)
  2. 内层 Column 作为已学习部分(橙色)
  3. 宽度通过 progress 计算属性动态设置

视觉效果:

  • 未学习:灰色背景条
  • 已学习:橙色进度条
  • 随单词切换实时更新

6.4 单词卡片

卡片正面(显示单词)
if (!this.showMeaning) {
  Column() {
    Text(this.WORDS[this.currentIndex].word)
      .fontSize(36)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.White)
    Text('点击翻转')
      .fontSize(14)
      .fontColor('#8E8E93')
      .margin({ top: 20 })
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}
卡片背面(显示释义)
if (this.showMeaning) {
  Column() {
    // 单词(小一号)
    Text(this.WORDS[this.currentIndex].word)
      .fontSize(26)
      .fontWeight(FontWeight.Bold)
      .fontColor('#FF9F0A')
    
    // 释义
    Text(this.WORDS[this.currentIndex].meaning)
      .fontSize(20)
      .fontColor(Color.White)
      .margin({ top: 16 })
    
    // 例句
    Text(this.WORDS[this.currentIndex].example)
      .fontSize(14)
      .fontColor('#8E8E93')
      .margin({ top: 16 })
      .textAlign(TextAlign.Center)
      .padding({ left: 16, right: 16 })
  }
  .width('100%')
  .height('100%')
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
}
卡片容器
Column() {
  // 正面或背面内容...
}
.width('88%')
.height(280)
.backgroundColor('#1C1C1E')  // 深灰色背景
.borderRadius(20)
.margin({ top: 20 })
.onClick(() => this.toggleCard())  // 点击翻转

设计细节:

  • 宽度88%:两侧留有边距
  • 高度280:固定高度,内容居中
  • 圆角20:现代卡片设计
  • 深灰背景:与黑色主体形成层次

6.5 动态按钮切换

这是本App的一大亮点:根据卡片状态显示不同的按钮组。

正面状态:上/下一个按钮
if (!this.showMeaning) {
  Row() {
    Button('⬅️ 上一个')
      .fontSize(15)
      .height(48)
      .borderRadius(24)
      .layoutWeight(1)
      .backgroundColor('#2C2C2E')
      .fontColor(Color.White)
      .margin({ right: 6 })
      .enabled(this.currentIndex > 0)  // 第一张禁用
      .onClick(() => this.prevCard())
    
    Button('➡️ 下一个')
      .fontSize(15)
      .height(48)
      .borderRadius(24)
      .layoutWeight(1)
      .backgroundColor('#2C2C2E')
      .fontColor(Color.White)
      .margin({ left: 6 })
      .enabled(this.currentIndex < this.WORDS.length - 1)  // 最后一张禁用
      .onClick(() => this.nextCard())
  }
  .width('90%')
  .margin({ top: 24 })
}
背面状态:认识/不认识按钮
if (this.showMeaning) {
  Row() {
    Button('🔴 不认识')
      .fontSize(15)
      .height(48)
      .borderRadius(24)
      .layoutWeight(1)
      .backgroundColor('#FF3B30')  // 红色
      .fontColor(Color.White)
      .margin({ left: 4, right: 4 })
      .onClick(() => this.nextCard())
    
    Button('🟢 认识')
      .fontSize(15)
      .height(48)
      .borderRadius(24)
      .layoutWeight(1)
      .backgroundColor('#34C759')  // 绿色
      .fontColor(Color.White)
      .margin({ left: 4, right: 4 })
      .onClick(() => this.nextCard())
  }
  .width('90%')
  .margin({ top: 24 })
}

设计理念:

状态按钮颜色用途
正面上一个/下一个灰色浏止单词
背面不认识/认识红色/绿色自我测评

七、完整代码实现

7.1 主页面完整代码(Index.ets)

// 数据接口定义
interface WordItem {
  word: string
  meaning: string
  example: string
}

@Entry
@Component
struct Index {
  // 内置词库
  private readonly WORDS: WordItem[] = [
    { word: 'abandon', meaning: 'v. 放弃;遗弃', example: 'They had to abandon the project.' },
    { word: 'benefit', meaning: 'n. 好处 v. 受益', example: 'Exercise benefits your health.' },
    { word: 'challenge', meaning: 'n. 挑战 v. 质疑', example: 'This is a big challenge.' },
    { word: 'dedicate', meaning: 'v. 致力于;献身', example: 'She dedicated her life to science.' },
    { word: 'efficient', meaning: 'adj. 高效的', example: 'This method is very efficient.' },
    { word: 'flexible', meaning: 'adj. 灵活的', example: 'We need a flexible schedule.' },
    { word: 'generate', meaning: 'v. 产生;发电', example: 'The wind generates electricity.' },
    { word: 'highlight', meaning: 'v. 强调 n. 亮点', example: 'The report highlights key issues.' },
    { word: 'influence', meaning: 'n./v. 影响', example: 'Media influences public opinion.' },
    { word: 'maintain', meaning: 'v. 维持;保养', example: 'Exercise helps maintain health.' },
    { word: 'negotiate', meaning: 'v. 谈判;协商', example: 'They negotiated a new contract.' },
    { word: 'obvious', meaning: 'adj. 明显的', example: 'It is obvious that he is right.' },
    { word: 'potential', meaning: 'n. 潜力 adj. 潜在的', example: 'She has great potential.' },
    { word: 'recognize', meaning: 'v. 认出;承认', example: 'I recognized her immediately.' },
    { word: 'significant', meaning: 'adj. 重要的;显著的', example: 'This is a significant discovery.' },
    { word: 'temporary', meaning: 'adj. 暂时的', example: 'This is only temporary.' },
    { word: 'ultimate', meaning: 'adj. 最终的 n. 终极', example: 'The ultimate goal is peace.' },
    { word: 'volunteer', meaning: 'n. 志愿者 v. 自愿', example: 'He volunteered to help.' },
    { word: 'withdraw', meaning: 'v. 撤回;取款', example: 'She withdrew money from the bank.' },
    { word: 'adapt', meaning: 'v. 适应;改编', example: 'You need to adapt to changes.' },
    { word: 'contribute', meaning: 'v. 贡献;捐献', example: 'Everyone should contribute.' },
    { word: 'demonstrate', meaning: 'v. 演示;证明', example: 'The experiment demonstrates the theory.' },
    { word: 'establish', meaning: 'v. 建立;设立', example: 'They established a new company.' },
    { word: 'frequent', meaning: 'adj. 频繁的', example: 'She makes frequent trips abroad.' },
    { word: 'guarantee', meaning: 'v./n. 保证;担保', example: 'We guarantee your satisfaction.' },
    { word: 'identify', meaning: 'v. 识别;确认', example: 'Can you identify the problem?' },
    { word: 'justify', meaning: 'v. 证明…正当', example: 'How can you justify this decision?' },
    { word: 'launch', meaning: 'v. 发射;发起', example: 'The company launched a new product.' },
    { word: 'motivate', meaning: 'v. 激励;激发', example: 'Good teachers motivate students.' },
    { word: 'obtain', meaning: 'v. 获得;得到', example: 'She obtained a degree in law.' }
  ]

  // 状态变量
  @State currentIndex: number = 0
  @State showMeaning: boolean = false

  // 计算属性:进度百分比
  get progress(): number {
    return Math.floor(((this.currentIndex + 1) / this.WORDS.length) * 100)
  }

  // 翻转卡片
  private toggleCard(): void {
    this.showMeaning = !this.showMeaning
  }

  // 下一个单词
  private nextCard(): void {
    if (this.currentIndex + 1 < this.WORDS.length) {
      this.currentIndex++
      this.showMeaning = false
    }
  }

  // 上一个单词
  private prevCard(): void {
    if (this.currentIndex > 0) {
      this.currentIndex--
      this.showMeaning = false
    }
  }

  build() {
    Column() {
      // 顶部标题栏
      Row() {
        Text('📚 单词卡')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
        Blank()
        Text(String(this.currentIndex + 1) + ' / ' + String(this.WORDS.length))
          .fontSize(14)
          .fontColor('#8E8E93')
      }
      .width('100%')
      .padding(16)

      // 进度条
      Row() {
        Column()
          .width(String(this.progress) + '%')
          .height(4)
          .borderRadius(2)
          .backgroundColor('#FF9F0A')
      }
      .width('100%')
      .height(4)
      .backgroundColor('#2C2C2E')
      .padding({ left: 16, right: 16 })

      // 单词卡片
      Column() {
        if (!this.showMeaning) {
          // 正面:显示单词
          Column() {
            Text(this.WORDS[this.currentIndex].word)
              .fontSize(36)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
            Text('点击翻转')
              .fontSize(14)
              .fontColor('#8E8E93')
              .margin({ top: 20 })
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
        } else {
          // 背面:显示释义和例句
          Column() {
            Text(this.WORDS[this.currentIndex].word)
              .fontSize(26)
              .fontWeight(FontWeight.Bold)
              .fontColor('#FF9F0A')
            Text(this.WORDS[this.currentIndex].meaning)
              .fontSize(20)
              .fontColor(Color.White)
              .margin({ top: 16 })
            Text(this.WORDS[this.currentIndex].example)
              .fontSize(14)
              .fontColor('#8E8E93')
              .margin({ top: 16 })
              .textAlign(TextAlign.Center)
              .padding({ left: 16, right: 16 })
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
        }
      }
      .width('88%')
      .height(280)
      .backgroundColor('#1C1C1E')
      .borderRadius(20)
      .margin({ top: 20 })
      .onClick(() => this.toggleCard())

      // 操作按钮
      if (!this.showMeaning) {
        // 正面:上一个/下一个
        Row() {
          Button('⬅️ 上一个')
            .fontSize(15)
            .height(48)
            .borderRadius(24)
            .layoutWeight(1)
            .backgroundColor('#2C2C2E')
            .fontColor(Color.White)
            .margin({ right: 6 })
            .enabled(this.currentIndex > 0)
            .onClick(() => this.prevCard())
          
          Button('➡️ 下一个')
            .fontSize(15)
            .height(48)
            .borderRadius(24)
            .layoutWeight(1)
            .backgroundColor('#2C2C2E')
            .fontColor(Color.White)
            .margin({ left: 6 })
            .enabled(this.currentIndex < this.WORDS.length - 1)
            .onClick(() => this.nextCard())
        }
        .width('90%')
        .margin({ top: 24 })
      } else {
        // 背面:不认识/认识
        Row() {
          Button('🔴 不认识')
            .fontSize(15)
            .height(48)
            .borderRadius(24)
            .layoutWeight(1)
            .backgroundColor('#FF3B30')
            .fontColor(Color.White)
            .margin({ left: 4, right: 4 })
            .onClick(() => this.nextCard())
          
          Button('🟢 认识')
            .fontSize(15)
            .height(48)
            .borderRadius(24)
            .layoutWeight(1)
            .backgroundColor('#34C759')
            .fontColor(Color.White)
            .margin({ left: 4, right: 4 })
            .onClick(() => this.nextCard())
        }
        .width('90%')
        .margin({ top: 24 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }
}

八、运行效果展示

8.1 构建项目

在 DevEco Studio 中:

  1. 点击菜单 Build > Build Hap(s)/APP(s) > Build Hap(s)
  2. 等待构建完成

8.2 运行应用

点击运行按钮,选择模拟器或真机运行。

8.3 界面效果

卡片正面(单词显示):

在这里插入图片描述

卡片背面(释义显示):

在这里插入图片描述

进度条效果:

在这里插入图片描述


九、核心知识点总结

9.1 ArkUI核心概念

概念说明本项目应用
@Entry页面入口装饰器Index页面
@Component组件装饰器struct Index
@State状态变量装饰器currentIndex, showMeaning
get计算属性progress

9.2 布局组件

组件用途本项目应用
Column垂直布局卡片容器、整体布局
Row水平布局标题栏、按钮组、进度条
Blank占位空白标题栏分隔
Text文本显示单词、释义、例句
Button按钮上一个/下一个/认识/不认识

9.3 样式系统

// 链式调用设置样式
Text('单词')
  .fontSize(36)
  .fontWeight(FontWeight.Bold)
  .fontColor(Color.White)
  .margin({ top: 20 })

// 条件样式
Button('上一个')
  .enabled(this.currentIndex > 0)  // 条件启用/禁用
  .backgroundColor('#2C2C2E')

9.4 条件渲染

if (!this.showMeaning) {
  // 显示正面内容
} else {
  // 显示背面内容
}

十、扩展思路

这个单词卡App还可以继续完善:

10.1 功能扩展

扩展项实现方案
真实翻转动画使用 animateTo 实现卡片翻转动画效果
单词分类按难度、词性、主题分类
学习统计记录认识/不认识的单词,生成学习报告
复习提醒基于艾宾浩斯遗忘曲线的复习提醒
发音功能集成TTS实现单词发音
收藏功能收藏难词,重点复习
自定义词库支持用户导入自己的单词表

10.2 技术优化

优化项说明
数据持久化使用 @ohos.data.preferences 保存学习进度
翻页动画添加 animateTo 实现平滑切换
手势支持左右滑动切换单词
搜索功能快速查找特定单词

十一、动画效果扩展(进阶)

如果想让卡片翻转更炫酷,可以添加动画效果:

11.1 基础翻转动画

@State rotateAngle: number = 0

private toggleCard(): void {
  animateTo({
    duration: 300,
    curve: Curve.EaseInOut
  }, () => {
    this.showMeaning = !this.showMeaning
    this.rotateAngle = this.showMeaning ? 180 : 0
  })
}

// 在卡片上添加旋转
Column() {
  // ...
}
.rotate({ angle: this.rotateAngle })

11.2 滑动手势

Column() {
  // ...
}
.gesture(
  PanGesture()
    .onActionEnd((event) => {
      if (event.offsetX < -50) {
        this.nextCard()  // 左滑下一个
      } else if (event.offsetX > 50) {
        this.prevCard()  // 右滑上一个
      }
    })
)

十二、常见问题

Q1: 进度条宽度不更新?

确保 progress 是计算属性(使用 get 关键字),这样每次状态变化都会重新计算。

Q2: 按钮禁用状态不生效?

检查 enabled() 方法的条件表达式是否正确返回布尔值。

Q3: 卡片切换时没有重置状态?

nextCard()prevCard() 方法中要重置 showMeaning = false

Q4: 如何添加更多单词?

WORDS 数组中继续添加 WordItem 对象即可。


十三、总结

通过这个单词记忆卡App的开发,我们学习了:

✅ ArkUI声明式UI开发模式
✅ 接口定义和数据结构设计
✅ 状态管理与计算属性
✅ 条件渲染实现动态UI
✅ 进度条可视化实现
✅ 按钮状态动态切换
✅ 边界检查与异常处理

这个项目虽然功能相对简单,但涵盖了鸿蒙开发的核心概念,非常适合作为入门练手项目。后续可以扩展翻转动画、学习统计、发音功能等,打造成一款完整的学习工具。


参考资料


如果这篇文章对你有帮助,欢迎点赞、收藏、评论!你的支持是我创作的动力! 📚

Logo

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

更多推荐