在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

1.1 当编程遇上教育

移动应用最有价值的用途之一就是教育。而面向儿童的教育应用有其独特的设计哲学——它必须同时满足三个要求:

  1. 有趣:儿童的注意力持续时间短,应用必须有足够的吸引力
  2. 直观:儿童不具备复杂的操作能力,交互必须简单明了
  3. 有反馈:儿童需要即时的正面反馈来维持学习动力

「组句子小课堂」正是基于这些原则设计的。它让小朋友通过点击按钮组合词语来构建句子,在游戏中学习语言组织能力。

1.2 本文目标

读完本文,你将掌握:

  1. 如何用 Flex + FlexWrap 构建适应不同屏幕的词语按钮网格
  2. 如何用 Scroll 实现横向滚动的分类标签栏
  3. 如何管理一个"句子"数组——添加词语、移除词语、清空、退格
  4. 如何用 AlertDialog 提供鼓励性反馈
  5. 儿童应用 UI 设计的基本原则和配色方案
  6. ArkUI 中的数据不可变更新模式

1.3 应用效果预览

┌──────────────────────────────────────────┐
│ 🧒 组句子 小课堂                          │
│ 点击词语按钮,组合成完整的句子吧!          │
│                                          │
│ ┌──────────────────────────────────────┐ │
│ │ 我 的 小猫 很 可爱。                    │ │
│ │ [我 ✕] [的 ✕] [小猫 ✕] [很 ✕] [可爱 ✕]│ │
│ └──────────────────────────────────────┘ │
│                                          │
│ [🗑️ 清空] [↩️ 退格] [⭐ 检查]              │
│                                          │
│ [人物] [动作] [物品] [地点] [形容词] [助词] [疑问] │
│                                          │
│ [我] [你] [他] [她] [它] [我们]           │
│ [你们] [他们] [妈妈] [爸爸] [老师] [同学]  │
│ [小猫] [小狗] [太阳] [月亮]               │
│                                          │
│ 点击词语加入句子 · 点击句子中的词语可移除    │
└──────────────────────────────────────────┘

二、项目架构

2.1 页面分层

Column(根容器)
├── 标题区(Row)
├── 提示文字
├── 句子展示区(Column + Flex)
│   ├── 空状态提示 / 已组句子 + 词语标签
├── 操作按钮区(Row)
│   ├── 清空、退格、检查
├── 分类标签栏(Scroll + Row)
│   ├── 7 个分类胶囊按钮
├── 词语按钮区(Flex + FlexWrap)
│   ├── 16 个词语按钮(当前分类)
└── 底部提示

2.2 数据模型

@State sentence: string[] = []   // 当前已选的词语列表
@State catIndex: number = 0      // 当前选中的分类索引
@State score: number = 0          // 鼓励计数器

private categories: string[] = ['人物', '动作', '物品', '地点', '形容词', '助词', '疑问']
private words: string[][] = [ /* 7 个分类 × 16 个词语 */ ]

整个应用的核心就是一个词语数组 sentence。用户的操作无非是:

  • 向数组尾部添加一个词(点击词语按钮)
  • 从数组中移除一个词(点击词语标签)
  • 清空整个数组(清空按钮)
  • 移除最后一个词(退格按钮)

2.3 数据流

用户点击词语按钮 "小猫"
  → addWord("小猫")
  → sentence = ["我", "的", "小猫"]
  → @State 更新 → UI 重新渲染
  → 句子显示区: "我 的 小猫。"
  → 词语标签区:显示 [我 ✕] [的 ✕] [小猫 ✕]

用户点击词语标签 "小猫 ✕"
  → removeWord(2)
  → sentence = ["我", "的"]
  → @State 更新 → UI 重新渲染

三、Flex 布局与词语按钮

3.1 Flex + FlexWrap 实现词云

词语按钮区域使用 Flex 布局并开启自动换行:

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
  ForEach(this.words[this.catIndex], (word: string, idx: number) => {
    Text(word).fontSize(17)
      .fontColor('#555').fontWeight(FontWeight.Medium)
      .padding({ left: 18, right: 18, top: 10, bottom: 10 })
      .backgroundColor('#fff').borderRadius(12)
      .shadow({ radius: 4, color: '#08000000', offsetY: 2 })
      .margin(5)
      .onClick(() => { this.addWord(word) })
  }, (word: string) => word)
}
.width('100%').padding(10).layoutWeight(1)

为什么用 Flex 而不是 Grid?

对比 Flex + FlexWrap Grid
换行行为 自动换行,按内容宽度排列 严格按列数排列
按钮宽度 由内容决定(不同词语宽度不同) 等宽
适合场景 标签云、词云、按钮组 宫格、相册

词语按钮的文字长度不同(如"为什么"比"的"长很多),Flex 让每个按钮按内容宽度排列,视觉上更自然。

3.2 按钮视觉设计

.fontSize(17).fontColor('#555').fontWeight(FontWeight.Medium)
.backgroundColor('#fff').borderRadius(12)
.shadow({ radius: 4, color: '#08000000', offsetY: 2 })
.margin(5)
  • 白色背景 + 浅阴影:模拟"卡片"质感,暗示可点击
  • 圆角 12vp:柔和友好,适合儿童
  • margin(5):按钮之间的间距,让每个按钮独立可辨

3.3 layoutWeight 填满空间

.layoutWeight(1)

这个属性让 Flex 区域填满操作按钮和分类标签栏之后的所有剩余空间。无论屏幕大小,词语按钮都会占满可用区域。


四、句子展示区

4.1 空状态 vs 有内容

if (this.sentence.length === 0) {
  Text('👆 点击下面的词语开始组句').fontSize(14).fontColor('#ccc')
} else {
  // 显示已组句子 + 词语标签
}

空状态提示用灰色文字和 emoji 引导用户操作。有内容后,展示已组句子和可选中的词语标签。

4.2 句子展示

Text(this.sentence.join(' ') + '。')
  .fontSize(22).fontWeight(FontWeight.Bold).fontColor('#333')
  .lineHeight(32).width('100%')
  • join(' ') 在词语之间加空格,让句子易读
  • 自动添加句号,让句子完整
  • 大字号 + 加粗,适合儿童阅读

4.3 词语标签(可移除)

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
  ForEach(this.sentence, (word: string, idx: number) => {
    Row() {
      Text(word).fontSize(15).fontColor('#FF6B6B')
      Text(' ✕').fontSize(12).fontColor('#ccc')
    }
    .padding({ left: 10, right: 6, top: 4, bottom: 4 })
    .backgroundColor('#FFF0F0').borderRadius(16)
    .margin(3)
    .onClick(() => { this.removeWord(idx) })
  }, (word: string, idx: number) => word + idx)
}

每个已选词语显示为一个带 ✕ 的标签

  • 粉色文字 + 浅粉背景,与整体配色一致
  • ✕ 符号暗示"可删除"
  • 点击标签触发 removeWord(idx) 从句子中移除该词

4.4 容器样式

.constraintSize({ minHeight: 80 })
.padding(14).backgroundColor('#fff').borderRadius(14)
.shadow({ radius: 6, color: '#10000000', offsetY: 3 })

白色卡片容器,最小高度 80vp,保证空状态也有足够的展示区域。


五、操作按钮

5.1 三个功能按钮

Row({ space: 10 }) {
  Button('🗑️ 清空').fontSize(13).fontColor('#fff')
    .backgroundColor('#FF6B6B').borderRadius(10).height(34)
    .onClick(() => { this.sentence = [] })

  Button('↩️ 退格').fontSize(13).fontColor('#666')
    .backgroundColor('#f5f5f5').borderRadius(10).height(34)
    .onClick(() => {
      if (this.sentence.length > 0) {
        let arr: string[] = []
        for (let i = 0; i < this.sentence.length - 1; i++) {
          arr.push(this.sentence[i])
        }
        this.sentence = arr
      }
    })

  Button('⭐ 检查').fontSize(13).fontColor('#fff')
    .backgroundColor('#4ECDC4').borderRadius(10).height(34)
    .onClick(() => { this.checkSentence() })
}
按钮 颜色 功能
🗑️ 清空 红色 #FF6B6B 清空所有已选词语
↩️ 退格 灰色 #f5f5f5 移除最后一个词语
⭐ 检查 青色 #4ECDC4 弹出鼓励弹窗

5.2 退格的实现

退格不能用 slice(0, -1)pop(),因为 ArkUI 要求不可变更新。需要显式创建新数组:

let arr: string[] = []
for (let i = 0; i < this.sentence.length - 1; i++) {
  arr.push(this.sentence[i])
}
this.sentence = arr

六、分类标签栏

6.1 横向滚动标签

Scroll() {
  Row({ space: 8 }) {
    ForEach(this.categories, (cat: string, ci: number) => {
      Text(cat).fontSize(14)
        .fontColor(ci === this.catIndex ? '#fff' : '#FF6B6B')
        .padding({ left: 18, right: 18, top: 6, bottom: 6 })
        .backgroundColor(ci === this.catIndex ? '#FF6B6B' : '#FFF0F0')
        .borderRadius(18)
        .onClick(() => { this.catIndex = ci })
    }, (cat: string) => cat)
  }
  .width('100%').padding({ left: 14, right: 14, top: 6, bottom: 4 })
}
.scrollable(ScrollDirection.Horizontal)

7 个分类标签在一行内显示,当超出屏幕宽度时自动启用横向滚动。

选中态 vs 未选中态:

状态 背景色 文字色 含义
选中 红色实心 #FF6B6B 白色 当前浏览的分类
未选中 红色浅底 #FFF0F0 红色 可切换的分类

点击标签切换 catIndex,下方的词语按钮区立即更新为对应分类的 16 个词语。

6.2 分类与词语的对应

private categories: string[] = ['人物', '动作', '物品', '地点', '形容词', '助词', '疑问']
private words: string[][] = [
  // index 0 → 人物
  ['我', '你', '他', '她', '它', '我们', '你们', '他们', '妈妈', '爸爸', '老师', '同学', '小猫', '小狗', '太阳', '月亮'],
  // index 1 → 动作
  ['吃', '喝', '看', '听', '说', '跑', '跳', '走', '飞', '游', '画', '唱', '读', '写', '玩', '笑'],
  // ... 以此类推
]

categories[catIndex]words[catIndex] 通过索引一一对应。这种设计比对象键值对更简洁,也避免了 ArkUI 不支持索引签名的限制。


七、核心逻辑实现

7.1 addWord:添加词语

addWord(w: string): void {
  let arr: string[] = []
  for (let i = 0; i < this.sentence.length; i++) {
    arr.push(this.sentence[i])
  }
  arr.push(w)
  this.sentence = arr
}

每次点击词语按钮,调用 addWord 将该词追加到句子末尾。

为什么不用 [...this.sentence, w]
ArkUI 的 ArkTS 编译器不支持数组展开运算符(spread operator),所以需要手动遍历复制。

7.2 removeWord:移除词语

removeWord(idx: number): void {
  let arr: string[] = []
  for (let i = 0; i < this.sentence.length; i++) {
    if (i !== idx) { arr.push(this.sentence[i]) }
  }
  this.sentence = arr
}

遍历当前句子数组,除了指定索引的词之外,其他所有词保留。新数组赋值后触发 UI 更新。

7.3 checkSentence:鼓励检查

checkSentence(): void {
  let text: string = ''
  for (let i = 0; i < this.sentence.length; i++) {
    text = text + this.sentence[i]
  }
  if (text.length < 2) { return }
  this.score++
  let msg: string = ''
  if (this.score % 3 === 0) {
    msg = '🌟 太棒了!你已经连对了 ' + this.score + ' 次!'
  } else if (this.score % 2 === 0) {
    msg = '👍 真厉害!继续加油!'
  } else {
    msg = '👏 很好!你组了 ' + this.score + ' 个句子了!'
  }
  AlertDialog.show({ message: msg, confirm: { value: '继续', action: () => {} } })
}

鼓励系统设计:

条件 消息 目的
每 3 次 🌟 太棒了 + 计数 里程碑奖励
每 2 次(非 3 倍数) 👍 真厉害 常规鼓励
其他 👏 很好 + 计数 基础鼓励

AlertDialog.show() 是 ArkUI 的弹窗 API。点击「继续」关闭弹窗,孩子可以继续组句。


八、儿童应用 UI 设计原则

8.1 配色方案

应用使用红 + 白 + 灰的主题色系:

颜色 色值 用途
红色 #FF6B6B 主色、标题、选中标签、按钮
红色浅底 #FFF0F0 未选中标签、词语标签背景
青色 #4ECDC4 "检查"按钮(正面反馈色)
白色 #fff 卡片背景、词语按钮背景
浅灰背景 #F8F9FA 页面背景

儿童配色的关键要点:

  1. 高对比度:文字与背景对比明显,易于阅读
  2. 暖色主调:红色为主色,传递温暖和活力
  3. 颜色数量适中:不超过 5 种主要颜色,避免视觉混乱
  4. 圆角大量使用:减少尖锐边缘,视觉友好

8.2 字号与可读性

元素 字号 说明
标题 24fp 醒目但不过大
已组句子 22fp 大字号,儿童易读
词语按钮 17fp 适中,点击精准
分类标签 14fp 标准标签字号
辅助文字 11~12fp 淡化处理

8.3 交互反馈

  1. 即时响应:点击按钮立即更新句子,没有延迟
  2. 视觉反馈:词语标签带 ✕ 符号,暗示可删除
  3. 鼓励弹窗:每次检查获得正面鼓励
  4. 空状态引导:初始时显示 👆 提示

九、核心编程技巧

9.1 不可变数组更新

ArkUI 通过引用比较检测 @State 变化。必须创建新数组,不能修改原数组:

// ❌ 错误:引用不变,UI 不更新
this.sentence.push('小猫')

// ❌ 错误:引用不变,UI 不更新
this.sentence.splice(idx, 1)

// ✅ 正确:创建新数组,引用变化
let arr: string[] = []
for (let i = 0; i < this.sentence.length; i++) {
  if (i !== idx) { arr.push(this.sentence[i]) }
}
this.sentence = arr

9.2 ForEach 键值

ForEach(this.sentence, (word: string, idx: number) => {
  // 词语标签 UI
}, (word: string, idx: number) => word + idx)

键值使用 word + idx 组合。因为句子中可能出现重复词语(如"我"出现两次),仅用 word 做键值会导致重复键冲突。加入索引 idx 确保唯一。

9.3 分类切换的数据映射

// 用户点击第 3 个分类 → catIndex = 2
// 词语按钮区渲染 words[2] → 物品类词语
Flex({ wrap: FlexWrap.Wrap }) {
  ForEach(this.words[this.catIndex], (word: string) => {
    Text(word).onClick(() => { this.addWord(word) })
  })
}

9.4 条件渲染空状态

if (this.sentence.length === 0) {
  Text('👆 点击下面的词语开始组句')
} else {
  // 显示句子和标签
}

通过 @State sentence 的长度控制两种 UI 状态的切换。


十、应用扩展方向

10.1 语音朗读

加入 TTS(Text To Speech)功能,朗读孩子造的句子:

import { textToSpeech } from '@kit.AccessibilityKit';

// 朗读句子
speakSentence(): void {
  let text = this.sentence.join('') + '。'
  textToSpeech.speak(text)
}

10.2 句子收藏

保存孩子造的好句子:

@State favorites: string[][] = []

saveSentence(): void {
  let arr: string[][] = []
  for (let i = 0; i < this.favorites.length; i++) {
    arr.push(this.favorites[i])
  }
  arr.push(this.sentence)
  this.favorites = arr
}

10.3 更多词库

扩展词库支持不同主题:

private themes: WordTheme[] = [
  { name: '动物', words: ['小猫', '小狗', '兔子', '大象', ...] },
  { name: '食物', words: ['苹果', '西瓜', '蛋糕', '冰淇淋', ...] },
  { name: '自然', words: ['太阳', '月亮', '星星', '花朵', ...] },
]

10.4 难度分级

  • 初级:只用人物 + 动作 + 物品,造简单句子
  • 中级:加入形容词和助词
  • 高级:加入疑问词,造问句

十一、常见问题与调试

11.1 点击按钮后句子不更新

排查

  1. addWord 是否创建了新数组?检查 this.sentence = arr
  2. @State sentence 是否正确声明?
  3. ForEach 键值是否唯一?

11.2 退格按钮无效

排查

  1. this.sentence.length > 0 条件是否满足?
  2. 新数组的元素数量是否为 length - 1

11.3 切换分类后词语不变

排查

  1. catIndex 是否正确更新?
  2. this.words[this.catIndex] 是否能正确访问二维数组?

11.4 弹窗不显示

排查

  1. checkSentence 是否被调用?
  2. text.length < 2 的条件是否满足(不足 2 个字不弹窗)?
  3. AlertDialog.show() 的参数格式是否正确?

11.5 分类标签遮挡

Scroll 容器设置了固定高度 .height(38),确保标签不会压缩其他区域。


十二、小结

12.1 核心知识点回顾

技术点 实现方式 用途
Flex 布局 Flex + FlexWrap + layoutWeight 词语按钮的流式排列
Scroll 标签 Scroll + Row + borderRadius 分类标签横向滚动
数组状态管理 @State string[] + 不可变更新 句子构建和修改
条件渲染 if/else 空状态 vs 已组句子
弹窗交互 AlertDialog.show() 鼓励反馈
分类切换 索引映射 words[catIndex] 切换词语类别
ForEach 键值 word + idx 组合 避免重复键冲突
约束尺寸 constraintSize({ minHeight }) 句子区最小高度

12.2 代码统计

SentenceBuilder.ets (159 行)
├── 数据定义 (18 行)
├── build() 主布局 (105 行)
│   ├── 标题区 (8 行)
│   ├── 句子展示区 (23 行)
│   ├── 操作按钮区 (20 行)
│   ├── 分类标签栏 (15 行)
│   ├── 词语按钮区 (13 行)
│   └── 底部提示 (2 行)
├── addWord (8 行)
├── removeWord (7 行)
└── checkSentence (17 行)

12.3 设计原则总结

  1. 儿童友好:暖色、圆角、大字号、即时反馈
  2. 交互直观:点击添加、点击移除、滑动切换分类
  3. 正面激励:鼓励弹窗 + 计数,每次操作都有正向反馈
  4. 数据驱动:所有 UI 变化由 @State 数据驱动
  5. 不可变更新:始终创建新数组,不修改原数组

附录

A:词库详情

分类 词数 示例词语
人物 16 我、你、他、她、我们、妈妈、爸爸、老师、同学、小猫、小狗、太阳、月亮
动作 16 吃、喝、看、听、说、跑、跳、走、飞、游、画、唱、读、写、玩、笑
物品 16 苹果、西瓜、牛奶、面包、书包、铅笔、玩具、水杯、帽子、雨伞
地点 16 家里、学校、公园、河边、超市、图书馆、动物园、花园、海边、教室
形容词 16 开心、漂亮、聪明、勇敢、可爱、认真、努力、快乐、温暖、明亮
助词 16 的、地、得、了、着、过、在、把、被、和、跟、对、从、比、向、为
疑问 16 吗、呢、吧、什么、怎么、为什么、哪儿、谁、几、多少、怎样

B:API 使用速查

API 用途
Flex({ wrap, justifyContent }) 流式布局容器
Scroll().scrollable() 可滚动容器
AlertDialog.show() 弹窗提示
constraintSize({ minHeight }) 最小尺寸约束
.layoutWeight(n) 权重填充剩余空间
ForEach(arr, builder, key) 列表渲染

C:推荐阅读

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐