HarmonyOS APP实战-画图APP - 第7篇:在画布上添加文字
1. 开篇
在第6篇中,我们完成了绘制模式的切换与交互反馈优化。通过 DrawMode 枚举( Rect 、 Circle 、 FreeDraw )管理当前绘制模式,在工具栏中实现了高亮选中状态,并在画布触摸事件中根据模式调用不同的绘制逻辑( drawRect 、 drawCircle 、 drawPath )。顶部显示当前模式标签,用户操作时获得明确的视觉反馈。
本篇我们将为画图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接口存储文字的所有属性,width和height用于触摸命中检测(判断用户是否点到文本区域)。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装饰器实现父子组件双向数据绑定,父组件提供fontSize和fontColor两个状态变量。 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组件需另定义,用于输入文字内容)。 - 触摸事件处理:当处于"添加文字"状态且内容非空时,触摸点即文字放置位置;否则尝试拖拽已有文字。
estimatedWidth和estimatedHeight是粗略估算的文字区域,用于触摸命中判断。精确计算可使用CanvasRenderingContext2D.measureText,但因异步问题本实现采用估算方式,仍可满足基本交互。drawAllTexts方法在每次重绘时调用,使用fillText绘制文字。font、fillStyle依据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组件接收canvasCtx和textItems引用,实现跨组件的协作。
3. 运行验证
在DevEco Studio中运行项目,按照以下步骤验证:
- 点击工具栏新增的"文字"按钮,模式切换为文字模式(按钮高亮)。
- 点击画布任意位置,弹出文字输入对话框。
- 输入文字内容(例如"Hello HarmonyOS"),通过滑块调整字体大小为48px,选择红色。
- 点击"确定"后,输入的文字出现在点击位置。
- 再次在文字区域内触摸并拖动,文字跟随手指移动。
- 重复上述步骤添加多个文字,可叠加在之前的图形之上。

预期表现:文字清晰渲染,颜色和大小与设置一致;拖拽行为流畅,触摸命中区域基本准确。
4. 小结与预告
本篇完成了文本绘制功能在画图APP中的集成。核心产出包括:
- 定义了
TextItem数据结构,支持存储内容、字体、颜色、坐标。 - 实现了
FontPicker组件,提供字体大小和颜色选择UI。 - 实现了
TextTool模块,包含文字输入对话框、触摸放置和拖拽逻辑。 - 使用
CanvasRenderingContext2D.fillTextAPI渲染文字,并与原有绘制逻辑共存。
至此,画图APP已支持矩形、圆形、自由画笔和文字四种绘制元素,用户的创作能力大大增强。
下一篇《使用effectKit处理图像》将引入 @ohos.effectKit 模块,为画布内容添加亮度调节、模糊化、灰度调节等图像特效。届时,用户在绘制完成后可选择应用滤镜,实现更丰富的视觉效果。我们将在第8篇中构建特效面板并集成到工具栏中。