鸿蒙App开发--绣迹网格编辑器怎么做?HarmonyOS Canvas绘制网格
·
十字绣网格编辑器怎么做?HarmonyOS Canvas绘制网格
如果你对十字绣感兴趣,可以去鸿蒙应用市场搜一下**「绣迹」**,下载下来体验体验。在网格上点选颜色、绘制图案,实时预览效果,体验完了再回来看这篇文章,你会更清楚这个网格编辑器是怎么用Canvas实现的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。
很多人觉得"前端转鸿蒙"应该很容易——都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂。
比如:
- Canvas绘图:Web的Canvas API和鸿蒙的
CanvasRenderingContext2D几乎一模一样——都是那个2D上下文,都是moveTo/lineTo/arc那套。但鸿蒙Canvas在画网格时,坐标系映射、触摸坐标的获取方式需要自己处理。 - 触摸事件:Web用
onClick获取鼠标坐标,鸿蒙用onTouch直接提供Canvas内的坐标。
别担心,接下来这篇文章,我会用"绣迹"的网格编辑器,带你看看HarmonyOS的Canvas怎么画网格、怎么处理触摸交互。
这篇文章聊什么
绣迹的核心功能是网格编辑器:
- 在Canvas上绘制十字绣网格
- 点击网格单元填充颜色
- 支持缩放和平移
- 实时预览效果
第一步:设计网格数据结构
// 十字绣图案数据
interface CrossStitchPattern {
id: string;
name: string;
width: number; // 网格宽度(格数)
height: number; // 网格高度(格数)
grid: string[][]; // 颜色ID二维数组
fabricType: string; // 绣布类型
createdAt: string;
}
// 创建空白图案
function createEmptyPattern(width: number, height: number): string[][] {
return Array.from({ length: height }, () =>
Array.from({ length: width }, () => 'empty')
);
}
第二步:用Canvas绘制网格
@Entry
@Component
struct GridEditorPage {
@State pattern: CrossStitchPattern | null = null
@State selectedColor: string = '#dc2626'
@State scale: number = 1
@State offsetX: number = 0
@State offsetY: number = 0
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
private cellSize: number = 20
build() {
Column() {
Canvas(this.context)
.width('100%')
.height(400)
.backgroundColor('#fef9c3')
.borderRadius(12)
.onReady(() => {
this.drawGrid();
})
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.handleTouch(event.touches[0].x, event.touches[0].y);
}
})
}
.padding(16)
}
private drawGrid() {
if (!this.pattern) return;
const ctx = this.context;
const { width, height, grid } = this.pattern;
const cellSize = this.cellSize * this.scale;
// 清空画布
ctx.clearRect(0, 0, width * cellSize, height * cellSize);
// 绘制网格线
ctx.strokeStyle = '#d1d5db';
ctx.lineWidth = 0.5;
// 垂直线
for (let x = 0; x <= width; x++) {
ctx.beginPath();
ctx.moveTo(x * cellSize + this.offsetX, this.offsetY);
ctx.lineTo(x * cellSize + this.offsetX, height * cellSize + this.offsetY);
ctx.stroke();
}
// 水平线
for (let y = 0; y <= height; y++) {
ctx.beginPath();
ctx.moveTo(this.offsetX, y * cellSize + this.offsetY);
ctx.lineTo(width * cellSize + this.offsetX, y * cellSize + this.offsetY);
ctx.stroke();
}
// 填充颜色
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const color = grid[y][x];
if (color && color !== 'empty') {
ctx.fillStyle = color;
ctx.fillRect(
x * cellSize + this.offsetX + 1,
y * cellSize + this.offsetY + 1,
cellSize - 2,
cellSize - 2
);
// 画十字标记
ctx.strokeStyle = 'rgba(255,255,255,0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x * cellSize + this.offsetX + 3, y * cellSize + this.offsetY + 3);
ctx.lineTo(x * cellSize + this.offsetX + cellSize - 3, y * cellSize + this.offsetY + cellSize - 3);
ctx.moveTo(x * cellSize + this.offsetX + cellSize - 3, y * cellSize + this.offsetY + 3);
ctx.lineTo(x * cellSize + this.offsetX + 3, y * cellSize + this.offsetY + cellSize - 3);
ctx.stroke();
}
}
}
}
private handleTouch(touchX: number, touchY: number) {
if (!this.pattern) return;
const cellSize = this.cellSize * this.scale;
const gridX = Math.floor((touchX - this.offsetX) / cellSize);
const gridY = Math.floor((touchY - this.offsetY) / cellSize);
// 检查是否在网格范围内
if (gridX >= 0 && gridX < this.pattern.width &&
gridY >= 0 && gridY < this.pattern.height) {
// 更新网格颜色
this.pattern.grid[gridY][gridX] = this.selectedColor;
this.drawGrid();
}
}
}
第三步:实现缩放功能
// 缩放控制
@Component
struct ZoomControls {
@Prop scale: number = 1
onZoomIn: () => void = () => {}
onZoomOut: () => void = () => {}
onReset: () => void = () => {}
build() {
Row() {
Button('-')
.width(40)
.height(40)
.onClick(this.onZoomOut)
Text(`${Math.round(this.scale * 100)}%`)
.fontSize(14)
.width(60)
.textAlign(TextAlign.Center)
Button('+')
.width(40)
.height(40)
.onClick(this.onZoomIn)
Button('重置')
.width(60)
.height(40)
.margin({ left: 8 })
.onClick(this.onReset)
}
.justifyContent(FlexAlign.Center)
}
}
// 在页面中使用
build() {
Column() {
ZoomControls({
scale: this.scale,
onZoomIn: () => {
this.scale = Math.min(3, this.scale + 0.2);
this.drawGrid();
},
onZoomOut: () => {
this.scale = Math.max(0.5, this.scale - 0.2);
this.drawGrid();
},
onReset: () => {
this.scale = 1;
this.offsetX = 0;
this.offsetY = 0;
this.drawGrid();
}
})
Canvas(this.context)
.width('100%')
.height(400)
.onReady(() => this.drawGrid())
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.handleTouch(event.touches[0].x, event.touches[0].y);
}
})
}
}
第四步:颜色选择器
// DMC绣线颜色
const DMC_COLORS = [
{ id: 'ecru', name: 'Ecru', hex: '#f5f0e0' },
{ id: 'blanc', name: 'Blanc', hex: '#ffffff' },
{ id: 'dmc_310', name: 'Black', hex: '#1f2937' },
{ id: 'dmc_321', name: 'Christmas Red', hex: '#dc2626' },
{ id: 'dmc_666', name: 'Red Bright', hex: '#ef4444' },
{ id: 'dmc_336', name: 'Blue', hex: '#1e40af' },
{ id: 'dmc_700', name: 'Green', hex: '#16a34a' },
{ id: 'dmc_972', name: 'Yellow', hex: '#facc15' },
{ id: 'dmc_550', name: 'Violet', hex: '#7e22ce' },
{ id: 'dmc_977', name: 'Brown', hex: '#d97706' },
];
@Component
struct ColorPalette {
@Prop selectedColor: string = ''
onColorSelect: (color: string) => void = () => {}
build() {
Column() {
Text('选择颜色')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(DMC_COLORS, (color) => {
Column() {
Circle({ width: 32, height: 32 })
.fill(color.hex)
.stroke(this.selectedColor === color.hex ? '#000000' : 'transparent')
.strokeWidth(2)
Text(color.name)
.fontSize(10)
.fontColor('#6b7280')
}
.margin({ right: 8, bottom: 8 })
.onClick(() => this.onColorSelect(color.hex))
})
}
}
}
}
第五步:保存和加载图案
// 保存图案
async function savePattern(context: Context, pattern: CrossStitchPattern): Promise<boolean> {
const patterns = await getItem<CrossStitchPattern[]>(context, 'patterns', []);
const index = patterns.findIndex(p => p.id === pattern.id);
if (index > -1) {
patterns[index] = pattern;
} else {
patterns.push(pattern);
}
return await setItem(context, 'patterns', patterns);
}
// 加载图案
async function loadPattern(context: Context, id: string): Promise<CrossStitchPattern | null> {
const patterns = await getItem<CrossStitchPattern[]>(context, 'patterns', []);
return patterns.find(p => p.id === id) || null;
}
// 导出图案为图片
async function exportPatternAsImage(context: CanvasRenderingContext2D, pattern: CrossStitchPattern): Promise<void> {
// Canvas可以导出为图片
// 具体实现取决于鸿蒙的Canvas导出API
}
第六步:常见问题
6.1 大图案性能问题
问题:图案很大时(如100x100),绘制变慢。
解决:只绘制可见区域,或者使用离屏Canvas。
// 只绘制可见区域
private drawVisibleArea() {
const startCol = Math.max(0, Math.floor(-this.offsetX / (this.cellSize * this.scale)));
const endCol = Math.min(this.pattern.width, startCol + Math.ceil(400 / (this.cellSize * this.scale)) + 1);
const startRow = Math.max(0, Math.floor(-this.offsetY / (this.cellSize * this.scale)));
const endRow = Math.min(this.pattern.height, startRow + Math.ceil(400 / (this.cellSize * this.scale)) + 1);
// 只绘制这个范围内的格子
for (let y = startRow; y < endRow; y++) {
for (let x = startCol; x < endCol; x++) {
// 绘制逻辑...
}
}
}
6.2 触摸精度问题
问题:手指太粗,点不到精确的格子。
解决:增加触摸区域的判断范围,或者提供放大镜功能。
总结
这篇文章围绕"绣迹"的网格编辑器,讲解了:
Canvas网格绘制
- 绘制垂直和水平网格线
- 填充颜色和十字标记
- 缩放和平移
触摸交互
- 触摸坐标转换为网格坐标
- 点击填充颜色
- 边界检查
性能优化
- 只绘制可见区域
- 离屏Canvas缓存
如果你对"绣迹"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。
更多推荐



所有评论(0)