欢迎光临
我们一直在努力

《星际练字板》三、沉浸光感儿童练字板案例指南

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,原因:

  • @Provide/@Consume 仅支持 @Component:这是 V1 装饰器,不能用于 @ComponentV2(V2 对应的是 @Provider/@Consumer,但当前 API 版本尚不完善)
  • @Observed 深度观察:配合 @State 可以追踪类对象属性的变化,满足画笔颜色、笔宽等实时更新需求
  • NavDestination 子页面不需要 @Entry:通过 @Provide/@Consume 共享 NavPathStack,子页面可以访问路由栈进行返回操作
  • 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 实现深度属性观察,避免不必要的全组件重渲染

    十、总结

    本案例通过以下技术栈实现了一款沉浸光感儿童练字板:

  • Canvas 绘图:手写交互 + 米字格辅助线 + 图片导出保存
  • 状态管理:@Observed 深度观察模型 + @Component 页面 + @State 状态 + @Provide/@Consume 跨组件共享
  • 沉浸光感 UI:多层渐变 + 霓虹发光 + 呼吸动画 + 磨砂玻璃
  • Navigation 路由:NavPathStack 管理页面跳转与参数传递
  • 核心知识点速记:

    • 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 指南
    赞(0)
    未经允许不得转载:171主机测评 » 《星际练字板》三、沉浸光感儿童练字板案例指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址