[鸿蒙开发实战篇]HarmonyOS文字书写功能实现指南

45 阅读9分钟

目录

  1. 功能概述
  2. 项目准备
  3. 第一步:创建数据模型
  4. 第二步:实现服务层
  5. 第三步:创建工具栏组件
  6. 第四步:实现绘图画布页面
  7. 第五步:配置路由
  8. 第六步:添加入口导航
  9. 功能测试
  10. 扩展功能

功能概述

文字书写功能包含两个核心模块:

1. 简笔画工坊(DrawingWorkshop)

  • Canvas 触摸绘图
  • 铅笔和橡皮工具
  • 8种颜色选择
  • 5种画笔粗细
  • 撤销/重做功能
  • 清空画布
  • 作品保存

2. 数字描红练习(NumberTracing)

  • 数字模板显示
  • 触摸描红进度追踪
  • 完成奖励系统

本指南主要实现简笔画工坊功能,这是文字书写的核心能力。


项目准备

2.1 创建目录结构

在 MyApplication 项目中创建以下目录结构:

entry/src/main/ets/
├── pages/
│   └── DrawingWorkshopPage.ets    (新建)
├── models/
│   └── DrawingModels.ets          (新建)
├── services/
│   └── DrawingService.ets         (新建)
└── components/
    └── DrawingToolbar.ets         (新建)

在 DevEco Studio 中,右键点击相应目录,选择 New > Directory 创建文件夹。


第一步:创建数据模型

1.1 创建 DrawingModels.ets

entry/src/main/ets/models/ 目录下创建 DrawingModels.ets 文件:

/**
 * 绘图工具枚举
 */
export enum DrawingTool {
  PENCIL = 'pencil',
  ERASER = 'eraser'
}

/**
 * 绘图点接口
 */
export interface DrawingPoint {
  x: number;
  y: number;
}

/**
 * 绘图路径接口
 */
export interface DrawingPath {
  tool: DrawingTool;
  points: DrawingPoint[];
  color: string;
  width: number;
}

/**
 * 颜色选项接口
 */
export interface ColorOption {
  name: string;
  color: string;
}

/**
 * 绘画教程步骤接口
 */
export interface DrawingStep {
  path: string;
  description: string;
}

/**
 * 绘画教程接口
 */
export interface DrawingTutorial {
  id: string;
  name: string;
  difficulty: 'easy' | 'medium' | 'hard';
  category: string;
  steps: DrawingStep[];
  preview: string;
}

/**
 * 预设颜色选项
 */
export const COLOR_OPTIONS: ColorOption[] = [
  { name: '黑色', color: '#333333' },
  { name: '红色', color: '#EF5350' },
  { name: '橙色', color: '#FF9B71' },
  { name: '黄色', color: '#FFD700' },
  { name: '绿色', color: '#52C41A' },
  { name: '蓝色', color: '#7DD3FC' },
  { name: '紫色', color: '#B19CD9' },
  { name: '粉色', color: '#FF6B9D' }
];

/**
 * 画笔粗细选项
 */
export const BRUSH_SIZES: number[] = [2, 4, 6, 8, 10];

/**
 * 绘画教程数据
 */
export const DRAWING_TUTORIALS: DrawingTutorial[] = [
  {
    id: 'drawing_cat',
    name: '小猫咪',
    difficulty: 'easy',
    category: 'animal',
    steps: [
      { path: 'M 50,50 Q 50,30 70,30 Q 90,30 90,50', description: '画圆圆的头' },
      { path: 'M 70,50 L 70,70 L 60,80 M 70,70 L 80,80', description: '画身体和腿' },
      { path: 'M 30,40 L 45,45 M 110,40 L 95,45', description: '画两只耳朵' }
    ],
    preview: 'drawings/cat.svg'
  },
  {
    id: 'drawing_sun',
    name: '太阳',
    difficulty: 'easy',
    category: 'nature',
    steps: [
      { path: 'M 50,50 m -30,0 a 30,30 0 1,0 60,0 a 30,30 0 1,0 -60,0', description: '画圆形太阳' },
      { path: 'M 50,10 L 50,0 M 50,90 L 50,100', description: '画上下光芒' },
      { path: 'M 10,50 L 0,50 M 90,50 L 100,50', description: '画左右光芒' }
    ],
    preview: 'drawings/sun.svg'
  },
  {
    id: 'drawing_house',
    name: '小房子',
    difficulty: 'medium',
    category: 'building',
    steps: [
      { path: 'M 20,50 L 20,90 L 80,90 L 80,50', description: '画房子主体' },
      { path: 'M 10,50 L 50,20 L 90,50', description: '画屋顶' },
      { path: 'M 40,90 L 40,70 L 55,70 L 55,90', description: '画门' },
      { path: 'M 60,55 L 75,55 L 75,70 L 60,70 Z', description: '画窗户' }
    ],
    preview: 'drawings/house.svg'
  }
];

关键说明:

  • DrawingTool 枚举定义了绘图工具类型
  • DrawingPath 记录每一笔的完整信息
  • COLOR_OPTIONSBRUSH_SIZES 是预设的颜色和画笔大小
  • DRAWING_TUTORIALS 提供简笔画教程数据

第二步:实现服务层

2.1 创建 DrawingService.ets

entry/src/main/ets/services/ 目录下创建 DrawingService.ets 文件:

import { DrawingPath, DrawingTutorial, DRAWING_TUTORIALS } from '../models/DrawingModels';

/**
 * 绘图服务类
 * 管理绘图数据和作品保存
 */
export class DrawingService {
  private static instance: DrawingService;

  private constructor() {}

  /**
   * 获取单例实例
   */
  public static getInstance(): DrawingService {
    if (!DrawingService.instance) {
      DrawingService.instance = new DrawingService();
    }
    return DrawingService.instance;
  }

  /**
   * 获取所有绘画教程
   */
  public getTutorials(): DrawingTutorial[] {
    return DRAWING_TUTORIALS;
  }

  /**
   * 根据难度筛选教程
   */
  public getTutorialsByDifficulty(difficulty: 'easy' | 'medium' | 'hard'): DrawingTutorial[] {
    return DRAWING_TUTORIALS.filter(t => t.difficulty === difficulty);
  }

  /**
   * 根据分类筛选教程
   */
  public getTutorialsByCategory(category: string): DrawingTutorial[] {
    return DRAWING_TUTORIALS.filter(t => t.category === category);
  }

  /**
   * 保存作品(简化版本,实际可扩展为数据库存储)
   */
  public async saveArtwork(
    userId: string,
    title: string,
    paths: DrawingPath[]
  ): Promise<boolean> {
    try {
      // 这里可以扩展为实际的数据库存储
      // 目前仅做日志记录
      console.info(`保存作品: ${title}, 用户: ${userId}, 路径数: ${paths.length}`);
      return true;
    } catch (error) {
      console.error('保存作品失败:', error);
      return false;
    }
  }

  /**
   * 导出作品为数据(可用于分享)
   */
  public exportArtwork(paths: DrawingPath[]): string {
    return JSON.stringify(paths);
  }

  /**
   * 导入作品数据
   */
  public importArtwork(data: string): DrawingPath[] | null {
    try {
      return JSON.parse(data) as DrawingPath[];
    } catch (error) {
      console.error('导入作品失败:', error);
      return null;
    }
  }
}

// 导出单例
export const drawingService = DrawingService.getInstance();

关键说明:

  • 使用单例模式管理绘图服务
  • 提供教程获取、作品保存等功能
  • 可扩展为数据库持久化存储

第三步:创建工具栏组件

3.1 创建 DrawingToolbar.ets

entry/src/main/ets/components/ 目录下创建 DrawingToolbar.ets 文件:

import { DrawingTool, ColorOption, COLOR_OPTIONS, BRUSH_SIZES } from '../models/DrawingModels';

/**
 * 绘图工具栏组件
 */
@Component
export struct DrawingToolbar {
  // 当前工具
  @Link currentTool: DrawingTool;
  // 当前颜色
  @Link currentColor: string;
  // 当前画笔大小
  @Link currentBrushSize: number;
  // 是否可撤销
  @Prop canUndo: boolean = false;
  // 是否可重做
  @Prop canRedo: boolean = false;

  // 回调函数
  onUndo?: () => void;
  onRedo?: () => void;
  onClear?: () => void;
  onSave?: () => void;

  @State private showColorPicker: boolean = false;
  @State private showBrushPicker: boolean = false;

  build() {
    Column() {
      // 主工具栏
      Row() {
        // 铅笔工具
        this.ToolButton({
          icon: '✏️',
          label: '铅笔',
          isActive: this.currentTool === DrawingTool.PENCIL,
          onClick: () => {
            this.currentTool = DrawingTool.PENCIL;
          }
        })

        // 橡皮工具
        this.ToolButton({
          icon: '🧽',
          label: '橡皮',
          isActive: this.currentTool === DrawingTool.ERASER,
          onClick: () => {
            this.currentTool = DrawingTool.ERASER;
          }
        })

        // 分隔线
        Divider()
          .vertical(true)
          .height(30)
          .margin({ left: 8, right: 8 })

        // 颜色选择
        this.ToolButton({
          icon: '🎨',
          label: '颜色',
          isActive: this.showColorPicker,
          onClick: () => {
            this.showColorPicker = !this.showColorPicker;
            this.showBrushPicker = false;
          }
        })

        // 画笔大小
        this.ToolButton({
          icon: '⚪',
          label: '大小',
          isActive: this.showBrushPicker,
          onClick: () => {
            this.showBrushPicker = !this.showBrushPicker;
            this.showColorPicker = false;
          }
        })

        // 分隔线
        Divider()
          .vertical(true)
          .height(30)
          .margin({ left: 8, right: 8 })

        // 撤销
        this.ToolButton({
          icon: '↩️',
          label: '撤销',
          isActive: false,
          isDisabled: !this.canUndo,
          onClick: () => {
            this.onUndo?.();
          }
        })

        // 重做
        this.ToolButton({
          icon: '↪️',
          label: '重做',
          isActive: false,
          isDisabled: !this.canRedo,
          onClick: () => {
            this.onRedo?.();
          }
        })

        // 清空
        this.ToolButton({
          icon: '🗑️',
          label: '清空',
          isActive: false,
          onClick: () => {
            this.onClear?.();
          }
        })

        // 保存
        this.ToolButton({
          icon: '💾',
          label: '保存',
          isActive: false,
          onClick: () => {
            this.onSave?.();
          }
        })
      }
      .width('100%')
      .padding(12)
      .backgroundColor('#F5F5F5')
      .justifyContent(FlexAlign.SpaceAround)

      // 颜色选择面板
      if (this.showColorPicker) {
        this.ColorPickerPanel()
      }

      // 画笔大小选择面板
      if (this.showBrushPicker) {
        this.BrushPickerPanel()
      }
    }
  }

  /**
   * 工具按钮构建器
   */
  @Builder
  ToolButton(params: {
    icon: string,
    label: string,
    isActive: boolean,
    isDisabled?: boolean,
    onClick: () => void
  }) {
    Column() {
      Text(params.icon)
        .fontSize(20)
      Text(params.label)
        .fontSize(10)
        .fontColor(params.isDisabled ? '#CCCCCC' : (params.isActive ? '#1890FF' : '#666666'))
    }
    .padding(8)
    .borderRadius(8)
    .backgroundColor(params.isActive ? '#E6F7FF' : 'transparent')
    .opacity(params.isDisabled ? 0.5 : 1)
    .onClick(() => {
      if (!params.isDisabled) {
        params.onClick();
      }
    })
  }

  /**
   * 颜色选择面板
   */
  @Builder
  ColorPickerPanel() {
    Row() {
      ForEach(COLOR_OPTIONS, (option: ColorOption) => {
        Stack() {
          Circle()
            .width(32)
            .height(32)
            .fill(option.color)

          if (this.currentColor === option.color) {
            Circle()
              .width(16)
              .height(16)
              .fill('#FFFFFF')
          }
        }
        .margin(6)
        .onClick(() => {
          this.currentColor = option.color;
          this.showColorPicker = false;
        })
      })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .justifyContent(FlexAlign.Center)
  }

  /**
   * 画笔大小选择面板
   */
  @Builder
  BrushPickerPanel() {
    Row() {
      ForEach(BRUSH_SIZES, (size: number) => {
        Stack() {
          Circle()
            .width(40)
            .height(40)
            .fill(this.currentBrushSize === size ? '#E6F7FF' : '#F5F5F5')

          Circle()
            .width(size * 2)
            .height(size * 2)
            .fill('#333333')
        }
        .margin(6)
        .onClick(() => {
          this.currentBrushSize = size;
          this.showBrushPicker = false;
        })
      })
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#FFFFFF')
    .justifyContent(FlexAlign.Center)
  }
}

关键说明:

  • 使用 @Link 双向绑定工具状态
  • @Builder 装饰器创建可复用的UI构建器
  • 颜色和画笔选择面板可展开/收起

第四步:实现绘图画布页面

4.1 创建 DrawingWorkshopPage.ets

entry/src/main/ets/pages/ 目录下创建 DrawingWorkshopPage.ets 文件:

import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import {
  DrawingTool,
  DrawingPoint,
  DrawingPath,
  COLOR_OPTIONS,
  BRUSH_SIZES
} from '../models/DrawingModels';
import { DrawingToolbar } from '../components/DrawingToolbar';
import { drawingService } from '../services/DrawingService';

/**
 * 简笔画工坊页面
 */
@Entry
@Component
struct DrawingWorkshopPage {
  // Canvas 设置
  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  // 绘图状态
  @State private currentTool: DrawingTool = DrawingTool.PENCIL;
  @State private currentColor: string = COLOR_OPTIONS[0].color;
  @State private currentBrushSize: number = BRUSH_SIZES[2];
  @State private isDrawing: boolean = false;

  // 路径管理
  @State private drawingPaths: DrawingPath[] = [];
  @State private redoStack: DrawingPath[] = [];
  private currentPath: DrawingPoint[] = [];

  // 画布尺寸
  @State private canvasWidth: number = 0;
  @State private canvasHeight: number = 0;

  build() {
    Column() {
      // 顶部导航栏
      this.HeaderBar()

      // 画布区域
      Stack() {
        Canvas(this.canvasContext)
          .width('100%')
          .height('100%')
          .backgroundColor('#FFFFFF')
          .onReady(() => {
            this.initCanvas();
          })
          .onTouch((event: TouchEvent) => {
            this.handleTouch(event);
          })
          .onAreaChange((oldArea: Area, newArea: Area) => {
            this.canvasWidth = newArea.width as number;
            this.canvasHeight = newArea.height as number;
          })
      }
      .layoutWeight(1)
      .margin(12)
      .borderRadius(12)
      .clip(true)
      .shadow({
        radius: 8,
        color: 'rgba(0,0,0,0.1)',
        offsetX: 0,
        offsetY: 2
      })

      // 工具栏
      DrawingToolbar({
        currentTool: $currentTool,
        currentColor: $currentColor,
        currentBrushSize: $currentBrushSize,
        canUndo: this.drawingPaths.length > 0,
        canRedo: this.redoStack.length > 0,
        onUndo: () => this.undo(),
        onRedo: () => this.redo(),
        onClear: () => this.clearCanvas(),
        onSave: () => this.saveArtwork()
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F0F2F5')
  }

  /**
   * 顶部导航栏
   */
  @Builder
  HeaderBar() {
    Row() {
      // 返回按钮
      Text('←')
        .fontSize(24)
        .fontColor('#333333')
        .padding(12)
        .onClick(() => {
          router.back();
        })

      // 标题
      Text('简笔画工坊')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .layoutWeight(1)
        .textAlign(TextAlign.Center)

      // 占位
      Text('')
        .width(48)
    }
    .width('100%')
    .height(56)
    .backgroundColor('#FFFFFF')
    .shadow({
      radius: 4,
      color: 'rgba(0,0,0,0.05)',
      offsetX: 0,
      offsetY: 2
    })
  }

  /**
   * 初始化画布
   */
  private initCanvas(): void {
    this.canvasContext.fillStyle = '#FFFFFF';
    this.canvasContext.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
  }

  /**
   * 处理触摸事件
   */
  private handleTouch(event: TouchEvent): void {
    switch (event.type) {
      case TouchType.Down:
        this.onTouchStart(event);
        break;
      case TouchType.Move:
        this.onTouchMove(event);
        break;
      case TouchType.Up:
      case TouchType.Cancel:
        this.onTouchEnd();
        break;
    }
  }

  /**
   * 触摸开始
   */
  private onTouchStart(event: TouchEvent): void {
    this.isDrawing = true;
    this.currentPath = [];

    if (event.touches && event.touches.length > 0) {
      const touch = event.touches[0];
      this.currentPath.push({
        x: touch.x,
        y: touch.y
      });
    }
  }

  /**
   * 触摸移动
   */
  private onTouchMove(event: TouchEvent): void {
    if (!this.isDrawing) return;

    if (event.touches && event.touches.length > 0) {
      const touch = event.touches[0];
      this.currentPath.push({
        x: touch.x,
        y: touch.y
      });
      this.drawCurrentPath();
    }
  }

  /**
   * 触摸结束
   */
  private onTouchEnd(): void {
    if (this.isDrawing && this.currentPath.length > 0) {
      // 保存当前路径
      const newPath: DrawingPath = {
        tool: this.currentTool,
        points: [...this.currentPath],
        color: this.currentTool === DrawingTool.PENCIL
          ? this.currentColor
          : '#FFFFFF',
        width: this.currentTool === DrawingTool.PENCIL
          ? this.currentBrushSize
          : 20
      };

      this.drawingPaths.push(newPath);
      // 清空重做栈
      this.redoStack = [];
    }

    this.isDrawing = false;
    this.currentPath = [];
  }

  /**
   * 绘制当前路径
   */
  private drawCurrentPath(): void {
    if (!this.canvasContext || this.currentPath.length < 2) return;

    // 设置绘制样式
    this.canvasContext.strokeStyle = this.currentTool === DrawingTool.PENCIL
      ? this.currentColor
      : '#FFFFFF';
    this.canvasContext.lineWidth = this.currentTool === DrawingTool.PENCIL
      ? this.currentBrushSize
      : 20;
    this.canvasContext.lineCap = 'round';
    this.canvasContext.lineJoin = 'round';

    // 绘制路径
    this.canvasContext.beginPath();
    this.canvasContext.moveTo(this.currentPath[0].x, this.currentPath[0].y);

    for (let i = 1; i < this.currentPath.length; i++) {
      this.canvasContext.lineTo(this.currentPath[i].x, this.currentPath[i].y);
    }

    this.canvasContext.stroke();
  }

  /**
   * 重绘整个画布
   */
  private redrawCanvas(): void {
    // 清空画布
    this.canvasContext.fillStyle = '#FFFFFF';
    this.canvasContext.fillRect(0, 0, this.canvasWidth, this.canvasHeight);

    // 重绘所有路径
    for (const path of this.drawingPaths) {
      this.drawPath(path);
    }
  }

  /**
   * 绘制单个路径
   */
  private drawPath(path: DrawingPath): void {
    if (path.points.length < 2) return;

    this.canvasContext.strokeStyle = path.color;
    this.canvasContext.lineWidth = path.width;
    this.canvasContext.lineCap = 'round';
    this.canvasContext.lineJoin = 'round';

    this.canvasContext.beginPath();
    this.canvasContext.moveTo(path.points[0].x, path.points[0].y);

    for (let i = 1; i < path.points.length; i++) {
      this.canvasContext.lineTo(path.points[i].x, path.points[i].y);
    }

    this.canvasContext.stroke();
  }

  /**
   * 撤销
   */
  private undo(): void {
    if (this.drawingPaths.length === 0) return;

    // 将最后一条路径移到重做栈
    const lastPath = this.drawingPaths.pop();
    if (lastPath) {
      this.redoStack.push(lastPath);
    }

    this.redrawCanvas();
  }

  /**
   * 重做
   */
  private redo(): void {
    if (this.redoStack.length === 0) return;

    // 从重做栈恢复路径
    const lastPath = this.redoStack.pop();
    if (lastPath) {
      this.drawingPaths.push(lastPath);
    }

    this.redrawCanvas();
  }

  /**
   * 清空画布
   */
  private clearCanvas(): void {
    // 显示确认对话框
    promptAction.showDialog({
      title: '清空画布',
      message: '确定要清空所有内容吗?此操作不可撤销。',
      buttons: [
        { text: '取消', color: '#666666' },
        { text: '确定', color: '#FF4D4F' }
      ]
    }).then((result) => {
      if (result.index === 1) {
        this.drawingPaths = [];
        this.redoStack = [];
        this.initCanvas();

        promptAction.showToast({
          message: '画布已清空',
          duration: 1500
        });
      }
    });
  }

  /**
   * 保存作品
   */
  private async saveArtwork(): Promise<void> {
    if (this.drawingPaths.length === 0) {
      promptAction.showToast({
        message: '请先画点什么吧',
        duration: 2000
      });
      return;
    }

    try {
      // 保存作品
      const success = await drawingService.saveArtwork(
        'default_user', // 可替换为实际用户ID
        '我的画作',
        this.drawingPaths
      );

      if (success) {
        promptAction.showToast({
          message: '作品已保存 ✨',
          duration: 2000
        });
      } else {
        promptAction.showToast({
          message: '保存失败,请重试',
          duration: 2000
        });
      }
    } catch (error) {
      console.error('保存作品失败:', error);
      promptAction.showToast({
        message: '保存失败,请重试',
        duration: 2000
      });
    }
  }
}

关键说明:

  1. Canvas 初始化

    • 使用 RenderingContextSettings 创建渲染上下文
    • onReady 回调中初始化画布
  2. 触摸事件处理

    • TouchType.Down: 开始绘制
    • TouchType.Move: 持续绘制
    • TouchType.Up: 结束绘制并保存路径
  3. 路径管理

    • drawingPaths: 已完成的路径列表
    • redoStack: 撤销的路径栈
    • currentPath: 当前正在绘制的点
  4. 绘制逻辑

    • 实时绘制当前笔画
    • 撤销/重做时重绘整个画布

第五步:配置路由

5.1 更新 main_pages.json

打开 entry/src/main/resources/base/profile/main_pages.json,添加新页面:

{
  "src": [
    "pages/Index",
    "pages/DrawingWorkshopPage"
  ]
}

第六步:添加入口导航

6.1 更新 Index.ets

修改 entry/src/main/ets/pages/Index.ets,添加导航按钮:

import { router } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  @State message: string = 'MyApplication';

  build() {
    Column() {
      // 标题
      Text(this.message)
        .fontSize(32)
        .fontWeight(FontWeight.Bold)
        .fontColor('#333333')
        .margin({ top: 60, bottom: 40 })

      // 功能入口按钮
      Button('进入简笔画工坊')
        .width('80%')
        .height(56)
        .fontSize(18)
        .fontColor('#FFFFFF')
        .backgroundColor('#1890FF')
        .borderRadius(28)
        .shadow({
          radius: 8,
          color: 'rgba(24, 144, 255, 0.3)',
          offsetX: 0,
          offsetY: 4
        })
        .onClick(() => {
          router.pushUrl({
            url: 'pages/DrawingWorkshopPage'
          });
        })

      // 说明文字
      Text('点击按钮开始你的创作之旅')
        .fontSize(14)
        .fontColor('#999999')
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
  }
}

功能测试

7.1 编译运行

  1. 在 DevEco Studio 中点击 Build > Build Hap(s)/APP(s) > Build Hap(s)
  2. 连接模拟器或真机
  3. 点击运行按钮

7.2 功能测试清单

功能测试步骤预期结果
页面导航点击"进入简笔画工坊"按钮跳转到绘图页面
铅笔绘制在画布上滑动手指绘制黑色线条
颜色切换点击颜色按钮,选择红色切换后绘制红色线条
画笔大小点击大小按钮,选择最粗线条变粗
橡皮擦切换橡皮,在线条上滑动擦除线条
撤销绘制后点击撤销删除最后一笔
重做撤销后点击重做恢复最后一笔
清空点击清空,确认清空所有内容
保存点击保存显示保存成功提示
返回点击左上角返回返回主页

扩展功能

8.1 添加数字描红功能

创建 NumberTracingPage.ets,实现数字描红练习:

// 简化版示例,完整代码参考 DigitalSprouting
@Entry
@Component
struct NumberTracingPage {
  @State private number: number = 0;
  @State private progress: number = 0;

  build() {
    Column() {
      // 数字显示
      Text(this.number.toString())
        .fontSize(200)
        .fontColor('#E0E0E0')
        .fontWeight(FontWeight.Bold)

      // 进度条
      Progress({ value: this.progress, total: 100 })
        .width('80%')
        .height(20)

      // 操作按钮
      Row() {
        Button('上一个')
          .onClick(() => {
            if (this.number > 0) {
              this.number--;
              this.progress = 0;
            }
          })

        Button('下一个')
          .onClick(() => {
            if (this.number < 9) {
              this.number++;
              this.progress = 0;
            }
          })
      }
      .margin({ top: 40 })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

8.2 添加音效反馈

集成音效服务,在以下场景播放音效:

  • 切换工具
  • 完成绘制
  • 保存成功
  • 清空画布

8.3 添加手势识别

扩展功能以支持:

  • 双指缩放画布
  • 长按工具按钮显示更多选项
  • 摇一摇清空画布

8.4 添加作品库

创建作品管理功能:

  • 保存作品到本地数据库
  • 查看历史作品
  • 分享作品

常见问题

Q1: Canvas 不显示?

A: 确保 Canvas 组件有明确的宽高,且 onReady 回调已执行。

Q2: 触摸坐标不准确?

A: 检查 Canvas 组件的父容器是否有额外的 padding 或 margin。

Q3: 撤销/重做不工作?

A: 确保每次 onTouchEnd 时都将路径添加到 drawingPaths

Q4: 编译报错找不到模块?

A: 检查 import 路径是否正确,确保所有文件都已创建。


总结

通过本指南,您已经成功实现了:

  1. ✅ 数据模型定义(DrawingModels)
  2. ✅ 绘图服务(DrawingService)
  3. ✅ 工具栏组件(DrawingToolbar)
  4. ✅ 绘图画布页面(DrawingWorkshopPage)
  5. ✅ 路由配置
  6. ✅ 入口导航

这个简笔画工坊具备了完整的绘图功能,您可以在此基础上继续扩展更多特性。


参考资源


效果 在这里插入图片描述 源代码 gitcode.com/daleishen/j… 班级链接 developer.huawei.com/consumer/cn…