HarmonyOS APP实战-画图APP - 第7篇:文本绘制功能

HarmonyOS APP实战-画图APP - 第7篇:在画布上添加文字

1. 开篇

在第6篇中,我们完成了绘制模式的切换与交互反馈优化。通过 DrawMode 枚举( RectCircleFreeDraw )管理当前绘制模式,在工具栏中实现了高亮选中状态,并在画布触摸事件中根据模式调用不同的绘制逻辑( drawRectdrawCircledrawPath )。顶部显示当前模式标签,用户操作时获得明确的视觉反馈。

本篇我们将为画图APP增加文字绘制功能。文字是图形创作中不可或缺的元素------添加标题、批注、签名等场景都需要它在画布上呈现。实现思路是:用户通过一个弹出窗口输入文字、选择字体大小和颜色,然后在画布上点击或拖拽放置文本。我们将使用 @ohos.graphics.drawing 模块的 drawText API在Canvas上渲染文字,并实现可拖拽定位的交互。


2. 核心实现

2.1 基础配置

首先导入绘制文字所需的模块,并扩展我们的状态管理。在 Index.ets 顶部添加导入。

typescript 复制代码
// Index.ets --- 基础配置与模块导入
import { CanvasRenderingContext2D } from '@ohos.graphics.drawing';

// 定义文字对象的数据结构
interface TextItem {
  content: string;         // 文字内容
  fontSize: number;        // 字体大小(px)
  color: string;          // 颜色(十六进制字符串)
  x: number;              // 文本左上角x坐标
  y: number;              // 文本左上角y坐标
  width?: number;         // 文本渲染宽度(用于碰撞检测)
  height?: number;        // 文本渲染高度(用于碰撞检测)
}

// 扩展DrawMode枚举,添加TEXT模式
enum DrawMode {
  Rect,
  Circle,
  FreeDraw,
  Text   // 新增:文字模式
}

关键点说明:

  • TextItem 接口存储文字的所有属性, widthheight 用于触摸命中检测(判断用户是否点到文本区域)。
  • DrawMode.Text 作为新的绘制模式,与之前的形状/画笔模式平级。
  • 后续我们需要在画布渲染循环中遍历 TextItem 数组并调用 drawText

2.2 FontPicker组件(字体大小与颜色选择器)

用户输入文字时,需要同时选择字体大小和颜色。我们创建一个独立的 FontPicker 组件,它显示一个自定义弹窗或内嵌UI。

typescript 复制代码
// components/FontPicker.ets --- 字体大小与颜色选择器
@Component
export struct FontPicker {
  @Consume fontSize: number;           // 从父组件继承的字体大小
  @Consume fontColor: string;          // 从父组件继承的字体颜色

  build() {
    Column() {
      // 字体大小滑块
      Row() {
        Text('字体大小:')
          .fontSize(16)
          .width(80)
        Slider({
          value: this.fontSize,
          min: 12,
          max: 120,
          step: 4,
          style: SliderStyle.OutSet
        })
          .width(200)
          .onChange((value: number) => {
            this.fontSize = value;  // 通过@Consume双向绑定
          })
      }
      .margin({ bottom: 12 })

      // 颜色选择器(预设色块)
      Row() {
        Text('字体颜色:')
          .fontSize(16)
          .width(80)
        // 使用Grid展示常用颜色
        ForEach(['#000000', '#FF0000', '#00FF00', '#0000FF',
                 '#FFFF00', '#FF00FF', '#00FFFF', '#FFFFFF'], (color: string) => {
          Circle({ width: 30, height: 30 })
            .fill(color)
            .stroke(this.fontColor === color ? Color.Black : Color.White)
            .strokeWidth(3)
            .margin(4)
            .onClick(() => {
              this.fontColor = color;
            })
        })
      }
    }
    .padding(12)
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
  }
}

关键点说明:

  • 使用 @Consume 装饰器实现父子组件双向数据绑定,父组件提供 fontSizefontColor 两个状态变量。
  • Slider 组件控制字体大小,范围12~120px,步进4px。
  • 颜色选择器用8个预设色块,点击直接赋值;实际项目可扩展为完整的色盘(参考第5篇的颜色选择逻辑)。
  • 此组件被嵌入到文本输入弹窗中,跟随弹窗显示。

2.3 文本输入与拖拽放置模块(TextTool)

核心逻辑:当用户点击工具栏的"文字"按钮后,模式切换为 Text 。点击画布任意位置,弹出输入框让用户输入文字内容并确认。文字对象随即出现在点击位置,用户可继续拖拽调整位置。

typescript 复制代码
// components/TextTool.ets --- 文字输入与拖拽放置模块
@Component
export struct TextTool {
  private canvasCtx: CanvasRenderingContext2D;
  @Link textItems: TextItem[];          // 画布上所有文字对象
  @State fontSize: number = 36;         // 当前字体大小
  @State fontColor: string = '#000000'; // 当前字体颜色
  @State isAddingText: boolean = false; // 是否正在输入文字
  @State inputContent: string = '';     // 输入框内容
  private textDialogController: CustomDialogController;

  build() {
    Column() {
      // 文字模式专用按钮
      Button('添加文字')
        .fontSize(16)
        .onClick(() => {
          this.isAddingText = true;
          // 弹出自定义对话框用于输入文字
          this.showTextInputDialog();
        })
    }
  }

  // 显示文字输入对话框(使用CustomDialog)
  showTextInputDialog() {
    this.textDialogController = new CustomDialogController({
      builder: TextInputDialog({
        fontSize: this.fontSize,
        fontColor: this.fontColor,
        onConfirm: (content: string) => {
          this.inputContent = content;
          this.isAddingText = true;
          this.textDialogController.close();
        },
        onCancel: () => {
          this.isAddingText = false;
          this.textDialogController.close();
        }
      }),
      autoCancel: true,
      alignment: DialogAlignment.Center,
      offset: { dx: 0, dy: -80 }
    });
    this.textDialogController.open();
  }

  // 在画布上放置文字的触摸事件处理
  handleCanvasTouch(event: TouchEvent): void {
    if (this.isAddingText && this.inputContent) {
      // 获取触摸点坐标(相对于画布)
      const touchX = event.touches[0].x;
      const touchY = event.touches[0].y;

      // 创建一个新的TextItem并添加到textItems数组
      const newText: TextItem = {
        content: this.inputContent,
        fontSize: this.fontSize,
        color: this.fontColor,
        x: touchX,
        y: touchY
      };
      this.textItems = [...this.textItems, newText];

      // 重置状态,准备下一次输入
      this.inputContent = '';
      this.isAddingText = false;

      // 重绘画布
      this.redrawCanvas();
    } else {
      // 尝试拖拽已有的文字对象
      this.dragTextItem(event);
    }
  }

  // 拖拽文字:在触摸移动时更新坐标
  dragTextItem(event: TouchEvent) {
    const touchX = event.touches[0].x;
    const touchY = event.touches[0].y;

    // 遍历文字列表,检查触摸点是否落在某个文字区域内
    for (let i = this.textItems.length - 1; i >= 0; i--) {
      const item = this.textItems[i];
      // 计算文字区域(估算:宽度=content长度*fontSize*0.6,高度=fontSize*1.2)
      const estimatedWidth = item.content.length * item.fontSize * 0.6;
      const estimatedHeight = item.fontSize * 1.2;

      // 判断触摸点是否在文字矩形区域内
      if (touchX >= item.x && touchX <= item.x + estimatedWidth &&
          touchY >= item.y - estimatedHeight && touchY <= item.y) {
        // 更新坐标
        if (event.type === TouchType.Move) {
          item.x = touchX - estimatedWidth / 2;
          item.y = touchY;
          this.textItems = [...this.textItems]; // 触发UI更新
          this.redrawCanvas();
        }
        break;
      }
    }
  }

  // 重新绘制画布(包含所有文字)
  redrawCanvas() {
    if (!this.canvasCtx) return;
    // 清空画布并重新绘制所有图形(此处简化,实际需结合之前的绘制指令)
    this.canvasCtx.clearRect(0, 0, this.canvasCtx.width, this.canvasCtx.height);
    // ... 重新绘制之前的形状 & 画笔轨迹(省略,与第3-6篇代码集成)
    this.drawAllTexts();
  }

  // 遍历文字列表,绘制所有文字
  drawAllTexts() {
    if (!this.canvasCtx) return;
    this.textItems.forEach((item: TextItem) => {
      this.canvasCtx.font = `${item.fontSize}px sans-serif`;
      this.canvasCtx.fillStyle = item.color;
      this.canvasCtx.textAlign = 'left';
      this.canvasCtx.textBaseline = 'top';
      this.canvasCtx.fillText(item.content, item.x, item.y);
    });
  }
}

关键点说明:

  • 使用 CustomDialogController 弹出输入对话框( TextInputDialog 组件需另定义,用于输入文字内容)。
  • 触摸事件处理:当处于"添加文字"状态且内容非空时,触摸点即文字放置位置;否则尝试拖拽已有文字。
  • estimatedWidthestimatedHeight 是粗略估算的文字区域,用于触摸命中判断。精确计算可使用 CanvasRenderingContext2D.measureText ,但因异步问题本实现采用估算方式,仍可满足基本交互。
  • drawAllTexts 方法在每次重绘时调用,使用 fillText 绘制文字。 fontfillStyle 依据 TextItem 属性动态设置。

2.4 在画布组件中集成文字绘制

最后,将 TextTool 集成到主画布组件中,在渲染循环中调用文字绘制。

typescript 复制代码
// Index.ets --- 主画布组件(核心代码片段)
@Component
struct DrawingCanvas {
  private canvasCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D();
  @State currentMode: DrawMode = DrawMode.Rect;
  @State textItems: TextItem[] = []; // 文字对象数组
  private textTool: TextTool = new TextTool();

  build() {
    Column() {
      // ... 工具栏(省略,包含模式切换按钮)
      
      // 文字模式下显示TextTool组件
      if (this.currentMode === DrawMode.Text) {
        TextTool({
          canvasCtx: this.canvasCtx,
          textItems: $textItems
        })
          .width('100%')
          .height(60)
      }

      // 画布区域
      Canvas(this.canvasCtx)
        .width('100%')
        .layoutWeight(1)
        .backgroundColor('#FFFFFF')
        .onTouch((event: TouchEvent) => {
          if (this.currentMode === DrawMode.Text) {
            this.textTool.handleCanvasTouch(event);
          } else {
            // 原有形状/画笔模式处理(略)
          }
        })
    }
    .width('100%')
    .height('100%')
  }
}

关键点说明:

  • TextItem 数组通过 @State 声明,驱动UI更新。
  • 触摸事件根据 currentMode 分发:文字模式调用 TextTool.handleCanvasTouch ,其他模式走原有逻辑。
  • TextTool 组件接收 canvasCtxtextItems 引用,实现跨组件的协作。

3. 运行验证

在DevEco Studio中运行项目,按照以下步骤验证:

  1. 点击工具栏新增的"文字"按钮,模式切换为文字模式(按钮高亮)。
  2. 点击画布任意位置,弹出文字输入对话框。
  3. 输入文字内容(例如"Hello HarmonyOS"),通过滑块调整字体大小为48px,选择红色。
  4. 点击"确定"后,输入的文字出现在点击位置。
  5. 再次在文字区域内触摸并拖动,文字跟随手指移动。
  6. 重复上述步骤添加多个文字,可叠加在之前的图形之上。

预期表现:文字清晰渲染,颜色和大小与设置一致;拖拽行为流畅,触摸命中区域基本准确。


4. 小结与预告

本篇完成了文本绘制功能在画图APP中的集成。核心产出包括:

  • 定义了 TextItem 数据结构,支持存储内容、字体、颜色、坐标。
  • 实现了 FontPicker 组件,提供字体大小和颜色选择UI。
  • 实现了 TextTool 模块,包含文字输入对话框、触摸放置和拖拽逻辑。
  • 使用 CanvasRenderingContext2D.fillText API渲染文字,并与原有绘制逻辑共存。

至此,画图APP已支持矩形、圆形、自由画笔和文字四种绘制元素,用户的创作能力大大增强。

下一篇《使用effectKit处理图像》将引入 @ohos.effectKit 模块,为画布内容添加亮度调节、模糊化、灰度调节等图像特效。届时,用户在绘制完成后可选择应用滤镜,实现更丰富的视觉效果。我们将在第8篇中构建特效面板并集成到工具栏中。

相关推荐
xd1855785551 小时前
梦境解析师-基于鸿蒙的梦境心理分析应用开发实践
人工智能·华为·harmonyos·鸿蒙
千逐682 小时前
鸿蒙实战:一多自适应与原子化布局 —— 一次开发多端部署的工程化落地
华为·harmonyos·鸿蒙
解局易否结局3 小时前
鸿蒙原生开发实战:Native 并发与高性能计算
华为·harmonyos
xo198820113 小时前
鸿蒙 AI 自动化开发:让 AI 学会自己编译、自己调试
人工智能·自动化·harmonyos
HarmonyOS_SDK3 小时前
差异化推送消息能力,助力支付宝腕上消息业务闭环
harmonyos
用户0934077735144 小时前
HarmonyOS WPS Open SDK 实践:办公审批附件「打开—编辑—回传」代码实现
typescript·harmonyos
ZZZMMM.zip5 小时前
基于鸿蒙PC与鸿蒙Flutter框架构建AI本地生活服务平台
人工智能·flutter·华为·harmonyos·鸿蒙
解局易否结局5 小时前
鸿蒙多设备形态适配实战:从手表到车机的差异化 UI 设计
ui·华为·harmonyos
不羁的木木5 小时前
HarmonyOS APP实战-基于Image Kit的图像处理APP - 第1篇:环境搭建与项目初始化
图像处理·华为·harmonyos
解局易否结局5 小时前
鸿蒙新特性实战:元服务与卡片开发完全指南
华为·harmonyos