HarmonyOS 实战:打造「星际练字板」—— 沉浸光感儿童练字应用全流程开发指南
适用版本:HarmonyOS 6.1+(API 23+) 开发语言:ArkTS + ArkUI 核心技术:Canvas 绘图、状态管理(@Component + @Observed + @Provide/@Consume)、沉浸式光感 UI、Navigation 路由
效果

一、项目概述
1.1 效果图预览
本案例将开发一款名为 「星际练字板」 的儿童汉字练习应用。应用以深邃宇宙为主题背景,通过渐变光晕、霓虹发光、呼吸动画等视觉手法营造沉浸感。孩子们可以在发光画布上练习"风、花、雪、月、星"五个优美汉字,完成全部练习后进入庆祝展示页。
核心界面:
| 练字主页 | 深色宇宙背景 + 发光 Canvas 画布 + 霓虹色工具栏 + 范字展示 |
| 成果展示页 | 金色庆祝标题 + 五幅作品展示 + 脉冲光晕动画 |
1.2 功能清单
- ✅ Canvas 画布手写练习,带发光笔迹效果
- ✅ 米字格辅助线(发光虚线风格)
- ✅ 范字大字展示(带紫色光晕)
- ✅ 6 种霓虹色画笔选择
- ✅ 3 档笔宽切换
- ✅ 光效开关控制
- ✅ 5 字逐字练习与进度指示
- ✅ 作品保存到沙箱 + 成果展示页
- ✅ 呼吸光感动画(环境光晕脉冲)
- ✅ 横屏全屏沉浸式体验
1.3 技术架构
┌─────────────────────────────────────────────┐
│ EntryAbility │
│ (全屏设置 + 横屏 + 加载 CalliBoard) │
└─────────────────┬───────────────────────────┘
│
┌─────────────▼─────────────────┐
│ Navigation │
│ (NavPathStack 路由) │
└──────┬───────────────┬────────┘
│ │
┌──────▼──────┐ ┌─────▼──────┐
│ CalliBoard │ │ ResultPage │
│ (练字主页) │ │ (成果展示) │
└──────┬──────┘ └────────────┘
│
┌──────▼──────────────┐
│ DrawingConfig │
│ (@Observed 模型) │
└─────────────────────┘
二、项目搭建
2.1 创建项目
使用 DevEco Studio 创建一个 Empty Ability 项目:
- Project name:CalliBoardApp
- Bundle name:com.example.calliboard
- Compatible SDK:5.0.0(12)
- Model:Stage 模型
2.2 配置横屏全屏
打开 entry/src/main/module.json5,在 abilities 中添加 orientation 属性:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"orientation": "landscape", // 横屏
"deviceTypes": ["phone", "tablet", "2in1"],
// …其他配置
}
]
}
}
2.3 配置 EntryAbility
修改 EntryAbility.ets,加载练字主页并设置全屏沉浸:
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
this.context.getApplicationContext()
.setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/CalliBoard', (err) => {
if (err.code) return;
let windowClass = windowStage.getMainWindowSync();
// 全屏布局
windowClass.setWindowLayoutFullScreen(true);
// 隐藏导航条
windowClass.setSpecificSystemBarEnabled('navigationIndicator', false);
// 存储避让区域供页面使用
let area = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topRectHeight', area.topRect.height);
});
}
}
关键说明:
- setWindowLayoutFullScreen(true):让应用铺满整个屏幕
- setSpecificSystemBarEnabled('navigationIndicator', false):隐藏底部导航条
- setColorMode(COLOR_MODE_LIGHT):固定浅色模式,确保深色 UI 效果一致
2.4 配置路由表
在 entry/src/main/resources/base/profile/main_pages.json 中注册主入口页面:
{
"src": [
"pages/CalliBoard"
]
}
重要提示:ResultPage 作为 NavDestination 子页面,通过 Navigation 路由动态加载,不需要在 main_pages.json 中注册。如果将 NavDestination 页面注册到路由表,编译器会强制要求该页面有 @Entry 装饰器,这与 NavDestination 的使用方式冲突。
—
## 三、数据模型设计(@Observed 深度观察)
### 3.1 @Observed 模型类
使用 `@Observed` 装饰器实现类属性的深度观察,配合 `@State` 在组件中使用:
```typescript
// model/DrawingModel.ets
@Observed
export class DrawingConfig {
penColor: string = '#00E5FF' // 画笔颜色
penWidth: number = 10 // 画笔粗细
shadowEnabled: boolean = true // 发光效果开关
}
@Observed
export class WordItem {
character: string // 汉字
completed: boolean = false // 是否已完成
imageData: string = '' // 保存的图片路径
constructor(character: string) {
this.character = character
}
}
@Observed 使用要点:
- @Observed 装饰的类,其属性变化可被 @State、@ObjectLink、@Prop 追踪
- 类对象必须通过 @State 声明,才能触发 UI 自动更新
- 在 build() 中读取的属性才会触发对应组件的重渲染
3.2 页面级状态与跨组件通信
使用 @State 管理页面本地状态,@Provide/@Consume 实现跨组件状态共享:
@Entry
@Component
struct CalliBoard {
// @Provide 向子组件(NavDestination)共享路由栈
@Provide('navStack') navStack: NavPathStack = new NavPathStack()
@State currentWordIndex: number = 0 // 当前字索引
@State hasContent: boolean = false // 画布是否有内容
@State glowOpacity: number = 0.3 // 光晕动画值
@State config: DrawingConfig = new DrawingConfig() // @Observed 模型
@State wordList: WordItem[] = [ // 汉字列表
new WordItem('风'), new WordItem('花'), new WordItem('雪'),
new WordItem('月'), new WordItem('星')
]
}
子组件通过 @Consume 获取共享状态:
@Component
export struct ResultPage {
@Consume('navStack') navStack: NavPathStack // 消费父组件提供的路由栈
@State imageUrls: string[] = []
// …
}
四、沉浸光感 UI 实现
4.1 多层背景架构
沉浸感的核心在于多层叠加:
第 1 层:深色宇宙渐变背景(linearGradient)
第 2 层:环境光晕(radialGradient + 呼吸动画)
第 3 层:主要内容(画布 + 工具栏 + 范字)
第一层:宇宙渐变
Column()
.linearGradient({
direction: GradientDirection.RightBottom,
colors: [
['#0B0B3B', 0.0], // 深夜蓝
['#1A0A3E', 0.4], // 深紫
['#0D1B4A', 0.8], // 藏蓝
['#05051A', 1.0] // 近黑
]
})
第二层:环境光晕
使用 radialGradient 创建柔和的光斑,配合 animateTo 实现呼吸效果:
// 呼吸动画
aboutToAppear(): void {
animateTo(
{ duration: 2500, iterations: –1, curve: Curve.EaseInOut,
playMode: PlayMode.AlternateReverse },
() => { this.glowOpacity = 0.7 }
)
}
// 紫色光晕
Column()
.width(350).height(350).borderRadius(175)
.radialGradient({
center: ['50%', '50%'], radius: 175,
colors: [['rgba(100, 0, 255, 0.18)', 0.0], ['rgba(100, 0, 255, 0)', 1.0]]
})
.opacity(this.glowOpacity) // 随动画值变化
4.2 发光画布边框
Canvas(this.canvasContext)
.backgroundColor('rgba(255, 255, 255, 0.06)') // 半透明底色
.borderRadius(18)
.border({ width: 1, color: 'rgba(0, 229, 255, 0.2)' }) // 微光边框
.shadow({ radius: 25, color: 'rgba(0, 229, 255, 0.2)', // 外层光晕
offsetX: 0, offsetY: 0 })
4.3 范字光效
Text(this.wordList[this.currentWordIndex].character)
.fontSize(180)
.fontColor('#E8E0FF')
.shadow({ radius: 35, color: 'rgba(179, 136, 255, 0.7)', // 紫色光晕
offsetX: 0, offsetY: 0 })
4.4 霓虹工具栏
底部工具栏使用 backdropBlur 实现磨砂玻璃效果:
Column()
.borderRadius({ topLeft: 20, topRight: 20 })
.backgroundColor('rgba(255, 255, 255, 0.04)')
.backdropBlur(30) // 背景模糊
颜色选择器的选中状态使用 shadow 实现发光效果:
Column()
.width(36).height(36).borderRadius(18)
.backgroundColor(color)
.border({ width: this.config.penColor === color ? 3 : 0, color: '#FFFFFF' })
.shadow(this.config.penColor === color
? { radius: 14, color: color, offsetX: 0, offsetY: 0 } // 选中发光
: undefined)
五、Canvas 绘图核心实现
5.1 绘制发光米字格
private drawGlowGrid(): void {
const ctx = this.canvasContext
const w = ctx.width
const h = ctx.height
// 外框发光线
ctx.strokeStyle = 'rgba(0, 229, 255, 0.25)'
ctx.lineWidth = 1.5
ctx.strokeRect(8, 8, w – 16, h – 16)
// 虚线辅助线(青色调,低透明度 = 光感效果)
ctx.setLineDash([6, 6])
ctx.strokeStyle = 'rgba(0, 229, 255, 0.12)'
ctx.lineWidth = 1
// 水平中线、垂直中线、两条对角线…
ctx.beginPath()
ctx.moveTo(12, h / 2)
ctx.lineTo(w – 12, h / 2)
ctx.stroke()
// … 其余线条类似
ctx.setLineDash([])
}
5.2 手写交互(onTouch 事件处理)
Canvas(this.canvasContext)
.onTouch((event: TouchEvent) => {
const t: TouchObject = event.touches[0]
switch (event.type) {
case TouchType.Down:
// 手指按下:开始新路径
this.canvasContext.beginPath()
this.canvasContext.moveTo(t.x, t.y)
this.hasContent = true
break
case TouchType.Move:
// 手指移动:绘制线段
this.canvasContext.lineTo(t.x, t.y)
this.canvasContext.lineWidth = this.config.penWidth
this.canvasContext.strokeStyle = this.config.penColor
this.canvasContext.lineCap = 'round'
this.canvasContext.lineJoin = 'round'
// 关键:发光笔迹效果
if (this.config.shadowEnabled) {
this.canvasContext.shadowColor = this.config.penColor
this.canvasContext.shadowBlur = 10
}
this.canvasContext.stroke()
break
case TouchType.Up:
// 手指抬起:关闭路径,清除阴影
this.canvasContext.closePath()
this.canvasContext.shadowBlur = 0
break
}
})
发光笔迹原理:在每次 stroke() 之前设置 shadowColor 和 shadowBlur,Canvas 会在绘制线条的同时产生柔和的光晕效果。shadowBlur 值越大,光晕越扩散。
5.3 清除与重绘
private clearAndRedrawGrid(): void {
this.canvasContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.drawGlowGrid() // 重绘辅助线
this.hasContent = false
}
5.4 导出与保存图片
private saveImage(): void {
const dataUrl = this.canvasContext.toDataURL()
const imgPath = this.context.tempDir + '/calli_' + Date.now() + '.png'
const file = fileIo.openSync(imgPath,
fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE)
const base64 = dataUrl.split(';base64,').pop()
if (base64) {
const imgBuf = buffer.from(base64, 'base64')
fileIo.writeSync(file.fd, imgBuf.buffer)
}
fileIo.closeSync(file)
this.wordList[this.currentWordIndex].imageData = 'file://' + imgPath
}
六、页面导航实现
6.1 路由配置(Navigation + NavPathStack)
@Entry
@Component
struct CalliBoard {
@Provide('navStack') navStack: NavPathStack = new NavPathStack()
@Builder
PagesMap(name: string) {
if (name === 'Result') {
ResultPage()
}
}
build() {
Navigation(this.navStack) {
// 主内容…
}
.mode(NavigationMode.Stack)
.navDestination(this.PagesMap)
.hideTitleBar(true)
.hideToolBar(true)
}
}
6.2 页面跳转与参数传递
// 练完最后一个字时跳转
private goNext(): void {
this.saveImage()
this.wordList[this.currentWordIndex].completed = true
if (this.currentWordIndex >= this.wordList.length – 1) {
const imgUrls: string[] = []
for (const item of this.wordList) {
imgUrls.push(item.imageData)
}
this.navStack.pushPath({ name: 'Result', param: imgUrls })
} else {
this.currentWordIndex++
this.clearAndRedrawGrid()
}
}
6.3 成果展示页(NavDestination)
@Component
export struct ResultPage {
@Consume('navStack') navStack: NavPathStack // 通过 @Consume 获取父组件的路由栈
@State imageUrls: string[] = []
build() {
NavDestination() {
// 展示所有练字作品…
}
.onReady((context: NavDestinationContext) => {
const param = context.pathInfo.param as Object
if (param) {
this.imageUrls = param as string[]
}
})
.hideTitleBar(true)
}
}
注意:pathInfo.param 的类型为 unknown,需要通过 as Object 进行显式类型断言后才能使用。
七、完整项目文件结构
entry/src/main/
├── ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # Ability 入口(全屏+横屏配置)
│ ├── model/
│ │ └── DrawingModel.ets # @Observed 数据模型
│ └── pages/
│ ├── CalliBoard.ets # 练字主页面
│ └── ResultPage.ets # 成果展示页
├── resources/
│ ├── base/
│ │ ├── element/
│ │ │ ├── string.json # 字符串资源
│ │ │ └── color.json # 颜色资源
│ │ └── profile/
│ │ └── main_pages.json # 路由表
│ └── dark/
│ └── element/
│ └── color.json
└── module.json5 # 模块配置(横屏+设备类型)
八、关键设计决策说明
8.1 状态管理方案选择
本案例使用 @Component + @Observed + @Provide/@Consume,原因:
8.2 沉浸光感设计要点
| linearGradient | 页面背景 | 深邃宇宙感 |
| radialGradient | 环境光晕 | 柔和光斑漂浮感 |
| shadow (offsetX/Y=0) | 元素外发光 | 霓虹灯管效果 |
| backdropBlur | 工具栏 | 磨砂玻璃质感 |
| animateTo (AlternateReverse) | 呼吸动画 | 光晕缓慢脉冲 |
| globalAlpha + 半透明色 | 画布底色 | 透出背景层次感 |
8.3 Canvas 笔迹发光原理
普通笔迹:strokeStyle = color
发光笔迹:strokeStyle = color + shadowColor = color + shadowBlur = 10
在 onTouch 的 Move 事件中,每次 stroke() 前设置阴影属性,Canvas 会为每一小段线条绘制光晕。Up 事件时清除 shadowBlur 避免影响后续绘制。
九、扩展优化建议
9.1 功能扩展方向
| 撤销/重做 | 使用 getImageData() 保存画布快照栈 |
| 笔画引导动画 | 用 Path2D + 定时器逐帧绘制笔画路径 |
| 多字帖主题 | 抽象 WordItem 列表为配置项,支持切换古诗/成语等 |
| 音效反馈 | 在 TouchType.Down 时播放短促音效 |
| 作品分享 | 使用 @kit.ShareKit 分享保存的图片 |
9.2 性能优化
- 避免在 onTouch 的 Move 事件中调用 beginPath()(会导致笔迹断裂)
- 对大量重复绘制使用离屏 Canvas(OffscreenCanvas)
- 通过 @Observed + @State 实现深度属性观察,避免不必要的全组件重渲染
十、总结
本案例通过以下技术栈实现了一款沉浸光感儿童练字板:
核心知识点速记:
- onReady 是 Canvas 绑制的起点
- shadowBlur + shadowColor = 发光效果
- @Observed + @State = 深度属性观察与 UI 自动更新
- @Provide/@Consume = 跨组件状态共享(仅限 @Component)
- animateTo + AlternateReverse = 呼吸动画
- toDataURL() + fileIo = 图片保存
参考资源
- Canvas 组件文档
- CanvasRenderingContext2D 文档
- 儿童练字板架构指南
- 状态管理 V2 指南 (仅限 @Component)
- animateTo + AlternateReverse = 呼吸动画
- toDataURL() + fileIo = 图片保存
参考资源
- Canvas 组件文档
- CanvasRenderingContext2D 文档
- 儿童练字板架构指南
- 状态管理 V2 指南







