HarmonyOS5 儿童画板app:手绘写字(附代码)
2025-06-28 20:48:35
108次阅读
0个评论
鸿蒙汉字书写应用开发指南
一、核心架构设计
1. 横屏初始化实现
aboutToAppear(): void {
window.getLastWindow(getContext()).then((windowClass) => {
windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE)
})
}
通过window
模块强制设置为横屏模式,优化汉字书写体验。
2. 组件化架构
- CalligraphyPractice:主入口组件,管理整体布局和状态
- BottomText:底部汉字选择组件
- FinishPage:书写完成展示页
二、Canvas绘图核心技术
1. 毛笔效果算法
onTouch((event) => {
const distance = Math.sqrt(
(touch.x - canvasWidth/2) ** 2 +
(touch.y - canvasHeight/2) ** 2
);
const brushWidth = selectedWidth * (1 - distance / Math.max(canvasWidth, canvasHeight) * 0.3);
context.lineWidth = brushWidth > 5 ? brushWidth : 5;
});
通过计算触摸点与画布中心的距离动态调整笔刷粗细,模拟真实毛笔效果。
2. 米字格绘制
getPoints = (r: number, l: number) => {
return [
[r - Math.sqrt(r*r/2), r - Math.sqrt(r*r/2)],
[l - points[0][0], points[0][1]],
// 其他顶点...
];
}
基于几何计算精确绘制米字格辅助线,帮助掌握汉字结构。
三、关键功能实现
1. 文件存储管理
savePicture(img: string) {
const imgPath = getContext().tempDir + '/' + Date.now() + '.jpeg';
const file = fileIo.openSync(imgPath, fileIo.OpenMode.CREATE);
const imgBuffer = buffer.from(img.split(';base64,').pop(), 'base64');
fileIo.writeSync(file.fd, imgBuffer.buffer);
fileIo.closeSync(file);
}
安全保存书写作品到应用沙箱目录。
2. 状态管理
@State modeIndex: number = 0; // 0:毛笔 1:橡皮擦
@State wordOpacity: number[] = [1, 0, 0, 0, 0];
响应式状态自动同步UI变化。
四、交互流程优化
1. 汉字切换逻辑
onClick(() => {
if (this.n === 4) {
pageInfos.pushPathByName('Finish', imgUrl);
} else {
this.n++;
wordOpacity[this.n] = 1;
wordOpacity[this.n-1] = 0;
}
});
2. 清除功能
context.clearRect(0, 0, canvasWidth, canvasHeight);
drawGuideLines(context, 20);
五、UI适配方案
1. 横屏布局
Canvas(context)
.aspectRatio(1) // 正方形画布
.height('90%') // 充分利用高度
2. 底部导航
Image($r('app.media.last'))
.width('8%') // 比例适配
.height('100%')
六、完整代码结构
点击查看核心实现import { fileIo } from '@kit.CoreFileKit';
import { buffer } from '@kit.ArkTS';
import { window } from '@kit.ArkUI';
@Entry
@Component
struct CalligraphyPractice {
@Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();
@State message: string = '汉字书写练习';
@State modeValue: string = '毛笔'; // 当前工具:毛笔 or 橡皮擦
@State modeIndex: number = 0; // 工具下标
@State selectedWidth: number = 25; // 笔刷粗细
@State selectedColor: string = '#8B0000'; // 毛笔颜色(暗红)
@State imgUrl: Array<string> = []; // 保存图像URL
@State wordOpacity: Array<number> = [1, 0, 0, 0, 0]; // 汉字选择透明度
@State clearOpacity: number = 0.5;
@State n: number = 0; // 当前汉字索引
@State nextImg: string = 'app.media.next';
@State imgHeight: Array<string> = ['70%', '100%'];
@State showGuide: boolean = true; // 是否显示笔画引导
@State imgOpacity: number = 0.5;
// 画布参数
private canvasWidth: number = 0;
private canvasHeight: number = 0;
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D();
// 练习的汉字列表
private words: Array<string> = ['永', '天', '地', '人', '和'];
// 页面初始化(横屏显示)
aboutToAppear(): void {
// 设置当前app以横屏方式显示
window.getLastWindow(getContext()).then((windowClass) => {
windowClass.setPreferredOrientation(window.Orientation.LANDSCAPE) // 设置为横屏
})
}
// 获取米字格顶点坐标
getPoints = (r: number, l: number) => {
let points: number[][] = [[], [], [], []];
points[0] = [r - Math.sqrt(r * r / 2), r - Math.sqrt(r * r / 2)];
points[1] = [l - points[0][0], points[0][1]];
points[2] = [points[1][1], points[1][0]];
points[3] = [l - points[0][0], l - points[0][1]];
return points;
}
// 构建路由表
@Builder
PagesMap(name: string) {
if (name === 'Finish') {
FinishPage()
}
}
// 绘制米字格辅助线
drawGuideLines = (ctx: CanvasRenderingContext2D, r: number) => {
const width = ctx.width;
const height = ctx.height;
let points = this.getPoints(r, width);
// 对角线1
let n = 100;
let step = width / n;
let start = points[0];
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
ctx.lineTo(width, height);
ctx.strokeStyle = '#D2B48C';
ctx.lineWidth = 1;
ctx.stroke();
// 对角线2
start = points[1];
ctx.beginPath();
ctx.moveTo(start[0], start[1]);
ctx.lineTo(0, height);
ctx.stroke();
// 竖中线
ctx.beginPath();
ctx.moveTo(width / 2, 0);
ctx.lineTo(width / 2, height);
ctx.stroke();
// 横中线
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
}
// 保存图片到沙箱
savePicture(img: string, n: number) {
const imgPath = getContext().tempDir + '/' + Date.now() + '.jpeg';
const file = fileIo.openSync(imgPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
const base64Image = img.split(';base64,').pop();
const imgBuffer = buffer.from(base64Image, 'base64');
fileIo.writeSync(file.fd, imgBuffer.buffer);
fileIo.closeSync(file);
this.imgUrl[n] = 'file://' + imgPath;
}
build() {
Navigation(this.pageInfos) {
Column() {
// 顶部标题和熊猫图标
Row() {
Image($r('app.media.panda'))
.margin({ top: 20 })
.aspectRatio(1)
.height('80%')
.margin({ top: 10, left: 30 });
// 书写区域
Stack() {
// 汉字提示
Text(this.words[this.n])
.fontSize(200)
.fontFamily('STKaiti') // 楷体字体
.opacity(this.showGuide ? 0.1 : 0);
// 画布
Canvas(this.context)
.aspectRatio(1)
.height('90%')
.backgroundColor('#FFF8DC') // 米黄色背景
.borderRadius(15)
.opacity(0.9)
.onReady(() => {
this.drawGuideLines(this.context, 20);
})
.onAreaChange((oldVal, newVal) => {
this.canvasWidth = newVal.width as number;
this.canvasHeight = newVal.height as number;
})
.onTouch((event) => {
const touch: TouchObject = event.touches[0];
switch (event.type) {
case TouchType.Down:
this.context.beginPath();
this.context.moveTo(touch.x, touch.y);
this.clearOpacity = 1;
break;
case TouchType.Move:
// 毛笔效果:移动时线条粗细变化
const distance = Math.sqrt(
(touch.x - this.canvasWidth/2) * (touch.x - this.canvasWidth/2) +
(touch.y - this.canvasHeight/2) * (touch.y - this.canvasHeight/2)
);
const brushWidth = this.selectedWidth * (1 - distance / Math.max(this.canvasWidth, this.canvasHeight) * 0.3);
this.context.lineWidth = brushWidth > 5 ? brushWidth : 5;
this.context.strokeStyle = this.modeIndex === 0 ? this.selectedColor : 'white';
this.context.lineTo(touch.x, touch.y);
this.context.stroke();
break;
case TouchType.Up:
this.context.closePath();
break;
}
});
}
.margin({ left: 20, top: 5 });
// 清除按钮
Column() {
Button('清除')
.opacity(this.clearOpacity)
.type(ButtonType.ROUNDED_RECTANGLE)
.fontColor('#8B0000')
.fontSize(28)
.backgroundColor('#FFEBCD')
.width('15%')
.height('18%')
.margin({ top: 20, left: 10 })
.onClick(() => {
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.drawGuideLines(this.context, 20);
this.clearOpacity = 0.5;
});
}
.height('100%');
}
.width('100%')
.height('80%');
// 底部导航区
Row() {
// 上一个按钮
Image($r('app.media.last'))
.onClick(() => {
if (this.n > 0) {
this.n--;
this.wordOpacity[this.n] = 1;
this.wordOpacity[this.n+1] = 0;
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.drawGuideLines(this.context, 20);
}
if (this.n < 4) {
this.nextImg = 'app.media.next';
}
if (this.n === 0) {
// 当切换到第一个汉字时,设置底部导航栏图片透明度为0.5
this.imgOpacity = 0.5;
}
})
.opacity(this.imgOpacity)
.backgroundColor('#FFEBCD')
.margin({ left: 20 })
.height('100%')
.borderRadius(5)
.borderWidth(8)
.borderColor('#FFEBCD');
// 汉字选择区
Row({ space: 10 }) {
Stack() {
BottomText({ imgH: this.imgHeight[0], wds: this.words[0] })
BottomText({ imgH: this.imgHeight[1], wds: this.words[0], wdSize: 45 })
.opacity(this.wordOpacity[0])
}
Stack() {
BottomText({ imgH: this.imgHeight[0], wds: this.words[1] })
BottomText({ imgH: this.imgHeight[1], wds: this.words[1], wdSize: 45 })
.opacity(this.wordOpacity[1])
}
Stack() {
BottomText({ imgH: this.imgHeight[0], wds: this.words[2] })
BottomText({ imgH: this.imgHeight[1], wds: this.words[2], wdSize: 45 })
.opacity(this.wordOpacity[2])
}
Stack() {
BottomText({ imgH: this.imgHeight[0], wds: this.words[3] })
BottomText({ imgH: this.imgHeight[1], wds: this.words[3], wdSize: 45 })
.opacity(this.wordOpacity[3])
}
Stack() {
BottomText({ imgH: this.imgHeight[0], wds: this.words[4] })
BottomText({ imgH: this.imgHeight[1], wds: this.words[4], wdSize: 45 })
.opacity(this.wordOpacity[4])
}
}
.margin({ left: 10 });
// 下一个/完成按钮
Image($r(this.nextImg))
.borderWidth(8)
.borderColor('#FFEBCD')
.backgroundColor('#FFEBCD')
.height('100%')
.borderRadius(5)
.onClick(() => {
// 保存当前书写内容
this.imgUrl[this.n] = this.context.toDataURL();
this.savePicture(this.imgUrl[this.n], this.n);
// 清空画布并准备下一个汉字
this.context.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
this.drawGuideLines(this.context, 20);
if (this.n === 4) {
// 所有汉字完成,跳转到结果页
this.pageInfos.pushPathByName('Finish', this.imgUrl);
} else {
// 切换到下一个汉字
this.nextImg = this.n + 1 === 4 ? 'app.media.finish' : 'app.media.next';
this.n++;
this.wordOpacity[this.n] = 1;
this.wordOpacity[this.n-1] = 0;
if (this.n > 0) {
this.imgOpacity = 1;
}
}
console.log('n: ' + this.n);
})
.margin({ left: 250 });
}
.width('100%')
.height('18%')
.backgroundColor('#FFEBCD');
}
}
.width('100%')
.mode(NavigationMode.Stack)
.navDestination(this.PagesMap)
.hideTitleBar(true)
.hideToolBar(true)
.backgroundColor('#8B4513'); // 棕色背景
}
}
// 底部汉字选择组件
@Component
export struct BottomText {
public imgH: string = '';
public wds: string = '';
public wdSize: number = 30;
build() {
Stack() {
Text(this.wds)
.height(this.imgH)
.fontSize(this.wdSize)
.fontFamily('STKaiti');
Image($r('app.media.mi'))
.height(this.imgH)
.opacity(0.3);
}
.aspectRatio(1)
.borderRadius(5)
.backgroundColor('#FFEBCD');
}
}
// 完成页组件
@Component
export struct FinishPage {
@Consume('pageInfos') pageInfos: NavPathStack;
private imgUrl: Array<string> = [];
@Builder
ImageItem(url: string) {
Image(url)
.backgroundColor('#FFF8DC')
.borderRadius(10)
.height('30%')
.width('18%');
}
build() {
NavDestination() {
Column() {
Image($r("app.media.panda"))
.height('25%')
.aspectRatio(1)
.margin({ top: 10 });
Text('你的书法作品:')
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontFamily('STKaiti')
.margin({ top: 10, bottom: 15 });
Row({ space: 10 }) {
this.ImageItem(this.imgUrl[0])
this.ImageItem(this.imgUrl[1])
this.ImageItem(this.imgUrl[2])
this.ImageItem(this.imgUrl[3])
this.ImageItem(this.imgUrl[4])
}
.margin({ top: 5 });
Button('重新练习')
.type(ButtonType.ROUNDED_RECTANGLE)
.fontColor('#8B0000')
.fontSize(30)
.backgroundColor('#FFF8DC')
.height('15%')
.width('30%')
.margin({ top: 30 })
.onClick(() => {
// 清空数据并返回首页
this.imgUrl = [];
this.pageInfos.pop()
});
}
.height('100%')
.width('100%')
.backgroundColor('#8B4513');
}
.onReady((context: NavDestinationContext) => {
this.pageInfos = context.pathStack;
this.imgUrl = context.pathInfo.param as Array<string>;
})
.hideTitleBar(true)
.hideToolBar(true);
}
}
七、设计要点总结
- 物理模拟:通过数学算法还原真实书写体验
- 辅助功能:米字格帮助掌握汉字结构
- 状态驱动:响应式数据自动更新UI
- 安全存储:沙箱机制保护用户数据
- 横屏优化:专为书写场景设计的布局
00
- 0回答
- 0粉丝
- 0关注
相关话题
- HarmonyOS5 购物商城app(一):商品展示(附代码)
- HarmonyOS5 运动健康app(二):健康跑步(附代码)
- HarmonyOS5 运动健康app(一):健康饮食(附代码)
- HarmonyOS5 运动健康app(三):健康睡眠(附代码)
- HarmonyOS5 购物商城app(二):购物车与支付(附代码)
- 纯血HarmonyOS5 打造小游戏实践:绘画板(附源文件)
- HarmonyOS5 音乐播放器app(一):歌曲展示与收藏功能(附代码)
- 纯血HarmonyOS5 打造小游戏实践:扫雷(附源文件)
- 鸿蒙HarmonyOS 5小游戏实践:记忆翻牌(附:源代码)
- HarmonyOS 5 多端适配原理与BreakpointSystem工具类解析:附代码
- 鸿蒙HarmonyOS 5小游戏实践:数字记忆挑战(附:源代码)
- 鸿蒙HarmonyOS 5小游戏实践:打砖块游戏(附:源代码)
- 「星辰启明时 代码绘鸿图」Harmony OS Next
- 鸿蒙HarmonyOS 5 小游戏实践:数字华容道(附:源代码)
- 鸿蒙HarmonyOS 5小游戏实践:动物连连看(附:源代码)