背单词是大多数语言学习者的日常,而"单词卡"(Flashcard)是其中最经典的工具形态——正面显示单词,背面显示释义,手指一翻即可切换。本文从零开始,带你用 ArkUI 11(HarmonyOS NEXT 的声明式 UI 框架)构建一个可运行的单词卡应用。


一、你将要构建什么

一个单词卡 App,核心交互:

功能 说明
卡片正反面 正面显示英文单词,点击翻转显示中文释义
左右滑动 切换上一张 / 下一张单词
记忆状态标记 每张卡片可标记"记住了"或"还需复习"
本地持久化 关闭 App 再打开,进度不丢失
平移动画 卡片切换时有平滑的出入动画

下面是单词卡App的功能架构图:

单词卡App

核心交互功能

数据管理

UI组件

卡片正反面翻转

左右滑动切换

记忆状态标记

平移动画效果

本地持久化存储

进度自动保存

内置词库管理

卡片组件
WordCard.ets

主页面
Index.ets

数据模型
WordData.ets

二、环境准备

2.1 安装 DevEco Studio

HarmonyOS 开发者官网 下载 DevEco Studio NEXT(对应 API 12+,即 ArkUI 11)。安装后打开,在 Settings → SDK Manager 中确保安装了以下 SDK:

HarmonyOS SDK API 12 (or later)
• ArkTS 3.2+
• Native API (可选)

2.2 创建项目

  1. File → New → Create Project
  2. 选择模板:Empty Ability(ArkTS)
  3. 填写项目名:WordCardApp
  4. Compile SDK:选 API 12
  5. Finish

创建后的目录结构:

WordCardApp/
├── entry/
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── pages/
│   │   │   │   └── Index.ets        ← 主页面
│   │   │   ├── model/
│   │   │   │   └── WordData.ets     ← 数据模型
│   │   │   └── view/
│   │   │       └── WordCard.ets     ← 卡片组件
│   │   └── resources/
│   └── oh-package.json5
└── build-profile.json5

下面是项目创建后的完整目录结构图:

WordCardApp/

entry/

build-profile.json5

src/main/

oh-package.json5

ets/

resources/

pages/

model/

view/

Index.ets
主页面

WordData.ets
数据模型

WordCard.ets
卡片组件

三、数据模型层

model/WordData.ets 中定义单词的数据结构和内置词库:

// model/WordData.ets

// 单词卡片数据结构
export interface Word {
  id: number;
  word: string;        // 英文单词
  phonetic?: string;   // 音标(可选)
  meaning: string;     // 中文释义
  example?: string;    // 例句(可选)
  remembered: boolean; // 是否已记住
}

// 初始词库(可扩展)
export const defaultWords: Word[] = [
  { id: 1, word: 'abandon',     phonetic: '/əˈbændən/',     meaning: '放弃;遗弃',    remembered: false },
  { id: 2, word: 'brilliant',   phonetic: '/ˈbrɪliənt/',     meaning: '灿烂的;杰出的', remembered: false },
  { id: 3, word: 'collapse',    phonetic: '/kəˈlæps/',       meaning: '倒塌;崩溃',    remembered: false },
  { id: 4, word: 'diverse',     phonetic: '/daɪˈvɜːrs/',     meaning: '不同的;多样的', remembered: false },
  { id: 5, word: 'elaborate',   phonetic: '/ɪˈlæbərət/',     meaning: '精心制作的;阐述', remembered: false },
];

四、卡片组件(核心 UI)

ArkUI 11 使用 @Component 装饰器定义可复用的自定义组件。单词卡的核心是两个状态:

  • 正面:显示单词 + 音标
  • 背面:显示释义 + 例句

翻转效果通过 animateTo + 旋转 rotate 实现。

// view/WordCard.ets

@Component
export struct WordCard {
  // 父组件传入的单词数据
  private word: Word = { id: 0, word: '', meaning: '', remembered: false };
  // 当前是否显示背面
  @State isFlipped: boolean = false;
  // 翻转动画的角度
  @State flipDegree: number = 0;

  // 翻转卡片
  flip() {
    // 如果当前角度 < 90,翻到 180(背面);否则翻回 0(正面)
    let target = this.flipDegree < 90 ? 180 : 0;
    animateTo({ duration: 400, curve: Curve.EaseInOut }, () => {
      this.flipDegree = target;
    });
    // 动画中途切换内容层(旋转超过 90° 时切换文字)
    setTimeout(() => {
      this.isFlipped = !this.isFlipped;
    }, 200);
  }

  build() {
    Column() {
      Stack() {
        // === 正面 ===
        if (!this.isFlipped) {
          Column() {
            Text(this.word.word)
              .fontSize(32)
              .fontWeight(FontWeight.Bold)
              .textAlign(TextAlign.Center)
            if (this.word.phonetic) {
              Text(this.word.phonetic)
                .fontSize(16)
                .fontColor('#888')
                .margin({ top: 8 })
            }
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
        }

        // === 背面 ===
        if (this.isFlipped) {
          Column() {
            Text(this.word.meaning)
              .fontSize(28)
              .fontColor('#2B6CB0')
              .fontWeight(FontWeight.Medium)
              .textAlign(TextAlign.Center)
            if (this.word.example) {
              Text(this.word.example)
                .fontSize(16)
                .fontColor('#666')
                .margin({ top: 16 })
                .textAlign(TextAlign.Center)
            }
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
        }
      }
      .width(300)
      .height(200)
      .backgroundColor('#FFF')
      .borderRadius(16)
      .shadow({ radius: 12, color: '#33000000', offsetX: 0, offsetY: 4 })
      .rotate({ x: 0, y: 1, z: 0, angle: this.flipDegree })
      .onClick(() => { this.flip(); })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

关键点:使用 rotate({ y: 1 }) 绕 Y 轴旋转,配合 animateTo 产生 3D 翻转效果。正反面内容通过 if/else 条件渲染,在旋转角度越过 90° 时切换,避免用户看到背面文字的镜像。


下面是卡片翻转的状态转换图:

用户点击卡片

旋转角度≥90°

用户点击卡片

旋转角度≤90°

正面显示

翻转动画

animateTo执行

角度≥90°时

动画结束

开始旋转

角度变化

切换内容

完成旋转

背面显示

显示:英文单词 + 音标
点击触发 flip() 方法

显示:中文释义 + 例句
再次点击返回正面

五、主页面(卡片管理与滑动导航)

主页面 Index.ets 负责:

  1. 维护单词列表和当前索引
  2. 处理"上一张 / 下一张"滑动
  3. 标记"记住了" / “还需复习”
  4. 持久化进度

5.1 持久化工具函数

ArkUI 11 推荐使用 @ohos.data.preferences 做轻量级 KV 存储:

// 在 Index.ets 顶部引入
import { preferences } from '@kit.ArkData';

const KEY_WORDS = 'word_list';

async function saveWords(context: Context, words: Word[]): Promise<void> {
  let store = await preferences.getPreferences(context, 'wordcard_db');
  await store.put(KEY_WORDS, JSON.stringify(words));
  await store.flush();
}

async function loadWords(context: Context): Promise<Word[] | null> {
  let store = await preferences.getPreferences(context, 'wordcard_db');
  let json = await store.get(KEY_WORDS, '');
  return json ? JSON.parse(json) as Word[] : null;
}

5.2 主页面完整代码

// pages/Index.ets

import { Word, defaultWords } from '../model/WordData';
import { WordCard } from '../view/WordCard';
import { preferences } from '@kit.ArkData';

@Entry
@Component
struct Index {
  @State wordList: Word[] = [];
  @State currentIndex: number = 0;
  // 滑动偏移量(用于手势跟踪)
  @State offsetX: number = 0;

  aboutToAppear() {
    this.loadData();
  }

  async loadData() {
    let saved = await loadWords(getContext(this));
    this.wordList = saved ?? defaultWords;
  }

  async saveData() {
    await saveWords(getContext(this), this.wordList);
  }

  // 切换到上一张
  prevCard() {
    if (this.currentIndex > 0) {
      animateTo({ duration: 250 }, () => {
        this.offsetX = 0;  // 重置位移
        this.currentIndex--;
      });
    }
  }

  // 切换到下一张
  nextCard() {
    if (this.currentIndex < this.wordList.length - 1) {
      animateTo({ duration: 250 }, () => {
        this.offsetX = 0;
        this.currentIndex++;
      });
    }
  }

  // 切换记忆状态
  toggleRemembered() {
    let idx = this.currentIndex;
    this.wordList[idx].remembered = !this.wordList[idx].remembered;
    this.wordList = [...this.wordList];  // 触发 @State 刷新
    this.saveData();
  }

  build() {
    Column() {
      // === 顶部进度条 ===
      Text(`${this.currentIndex + 1} / ${this.wordList.length}`)
        .fontSize(14)
        .fontColor('#999')
        .margin({ top: 24 });

      // === 进度指示条 ===
      Progress({
        value: this.currentIndex + 1,
        total: this.wordList.length,
        type: ProgressType.Linear
      })
        .width('70%')
        .height(6)
        .color('#4A90D9')
        .margin({ top: 8, bottom: 24 });

      // === 单词卡片(带手势滑动) ===
      if (this.wordList.length > 0) {
        Stack() {
          // 当前卡片(带拖拽手势)
          WordCard({ word: this.wordList[this.currentIndex] })
            .offset({ x: this.offsetX })
            .gesture(
              GestureGroup(GestureMode.Sequence,
                PanGesture({ distance: 30 })
                  .onActionUpdate((event: GestureEvent) => {
                    this.offsetX = event.offsetX;
                  })
                  .onActionEnd(() => {
                    if (this.offsetX > 80) {
                      this.prevCard();
                    } else if (this.offsetX < -80) {
                      this.nextCard();
                    } else {
                      // 回弹
                      animateTo({ duration: 200 }, () => {
                        this.offsetX = 0;
                      });
                    }
                  })
              )
            )
        }
        .width('100%')
        .height(240)
        .layoutWeight(1)
        .justifyContent(FlexAlign.Center)
      }

      // === 底部按钮区 ===
      Row() {
        // 上一张
        Button({ type: ButtonType.Circle, stateEffect: true }) {
          Text('◀').fontSize(24).fontColor('#FFF')
        }
        .width(52).height(52)
        .backgroundColor('#E2E8F0')
        .onClick(() => { this.prevCard(); })

        // 标记"记住了" / "还需复习"
        Button({ type: ButtonType.Circle, stateEffect: true }) {
          Text(this.wordList[this.currentIndex]?.remembered ? '✓' : '○')
            .fontSize(24)
            .fontColor('#FFF')
        }
        .width(64).height(64)
        .backgroundColor(
          this.wordList[this.currentIndex]?.remembered ? '#48BB78' : '#A0AEC0'
        )
        .margin({ left: 32, right: 32 })
        .onClick(() => { this.toggleRemembered(); })

        // 下一张
        Button({ type: ButtonType.Circle, stateEffect: true }) {
          Text('▶').fontSize(24).fontColor('#FFF')
        }
        .width(52).height(52)
        .backgroundColor('#E2E8F0')
        .onClick(() => { this.nextCard(); })
      }
      .margin({ bottom: 48 })
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F7FAFC')
  }
}

下面是应用的数据流和交互流程图:

业务逻辑层

UI层

点击翻转

左右滑动

上一张/下一张

切换记忆状态

状态更新

保存数据

读取数据

提供数据

数据流

用户操作

状态变更
@State更新

UI重渲染
build()

动画执行
animateTo

数据持久化
saveData()

卡片组件
WordCard

主页面
Index

滑动手势

按钮交互

标记按钮

数据管理

持久化存储
preferences

内置词库
defaultWords

六、运行效果

在 DevEco Studio 中点击 Run(选择模拟器或真机),你将看到:

  1. 一张白色卡片浮在浅灰背景中央,显示第一个单词 abandon
  2. 点击卡片 —— 卡片沿 Y 轴翻转,显示中文释义"放弃;遗弃"
  3. 左右拖动卡片 —— 跟随手指位移;超过 80px 松手即切换到上一张/下一张,不足则回弹
  4. 底部圆形按钮 —— ◀ 上一张 / ▶ 下一张 / ○ 标记记住(变绿 ✓)
  5. 关闭 App 再打开 —— 进度和记忆状态依然保留

七、扩展思路

完成了基础版,你可以继续添加:

功能 实现方向
添加单词 弹窗输入框 + this.wordList.push() + 持久化
分组词库 @ohos.data.relationalStore(关系型数据库)建立词库表
学习统计 @State 统计今日学习数、复习数,用 Chart 组件展示
语音朗读 调用 @ohos.multimedia.mediacreateAudioPlayer + TTS 服务
暗黑模式 使用 @Styles 定义两套主题色,通过 @LocalStorageProp 切换
云同步 接入 @kit.NetworkKit 的 HTTP 请求,同步到远端服务器

八、小结

通过这个项目,你实践了 ArkUI 11 的以下核心概念:

  • @Component / @Entry —— 声明式组件定义
  • @State / 数据驱动 UI —— 状态变化自动触发视图更新
  • animateTo + rotate —— 声明式 3D 翻转动画
  • PanGesture + GestureGroup —— 手势识别与响应链
  • preferences —— 轻量级本地 KV 持久化
  • Column / Row / Stack / Text / Button / Progress —— 常用布局和组件

单词卡 App 虽小,但"状态管理 → 手势交互 → 动画反馈 → 数据持久化"这条链路覆盖了大部分 ArkUI 应用开发的骨架。由此出发,你可以构建更复杂的笔记、题库、闪卡学习等应用。

Happy coding on HarmonyOS NEXT! 🚀

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐