使用Drawing实现图形绘制与显示
场景绘制能力对比
Canvas画布组件是用来显示自绘内容的组件,它具有保留历史绘制内容、增量绘制的特点。Canvas 有 CanvasRenderingContext2D/OffscreenCanvasRenderingContext2D 和 Drawing 两套API,应用使用两套绘制API绘制的内容都可以在绑定的Canvas组件上显示。其中 CanvasRenderingContext2D 按照W3C标准封装了 Native Drawing 接口,可以方便快速复用Web应用的绘制逻辑,因此非常适用于Web应用和游戏、快速原型设计、数据可视化、在线绘图板、教学工具或创意应用等场景。然而,由于它的性能依赖于浏览器的实现,不如原生API那样接近硬件,因此对于性能要求比较高绘制比较复杂或者硬件依赖性比较强的场景如高性能游戏开发、专业图形处理软件、桌面或移动应用等,使用 Canvas CanvasRenderingContext2D 绘制会存在一定的卡顿、掉帧等性能问题,此时可以直接使用 Native Drawing 接口自绘制替代 Canvas CanvasRenderingContext2D 绘制来提升绘制性能。
方案 | 适用场景 | 特点 |
Canvas CanvasRenderingContext2D+canvas组件 | 快速原型设计、数据可视化、在线绘图板、教学工具、创意应用 | 场景简单、跨平台、快捷灵活、兼容性强、开发维护成本低、性能要求低。 |
arkTsDrawing+RenderNode | 在线绘图板、教学工具 | 开发效率高 性能在canvas组件和native drawing之间 |
Native Drawing +Xcomponent | 高性能游戏开发、专业图形处理软件、桌面或移动应用开发 | 场景复杂、资源管理精细、硬件依赖强、与平台 |
场景一:实现自定义字体
方案
- 通过 font.registerFont注册自定义字体。
- 使用TextStyle接口创建一个文本样式实例TextStyle,TextStyle可以设置字体的颜色、大小、样式、类型。
- 使用ParagraphStyle接口创建一个段落样式实例myParagraphStyle,并设置文本样式等属性,使用FontCollection接口创建一个字体管理器实例fontCollection。
- 使用ParagraphBuilder的接口,以myParagraphStyle和fontCollection为参数创建一个段落生成器实例ParagraphGraphBuilder,并调用其接口使文本样式更新以及添加段落文本,在调用build()接口生成段落实例paragraph,最后调用paragraph.paint接口在屏幕上显示。
核心代码
//注册自定义字体
font.registerFont({
familyName: 'Condensed_Black', // 注册的字体名称
familySrc: '/utils/TsangerYuYangT_W05_W05.ttf' // font文件夹与pages目录同级
})
let chaptersTextStyle: text.TextStyle = {
color: { alpha: 255, red: 0, green: 0, blue: 0 },
fontSize: 80,
fontStyle: 1,
fontFamilies: ['Condensed_Black']
};
//断词类型,换行策略,文本方向以及对齐方式由此设置
let headParagraphStyle: text.ParagraphStyle = {
textStyle: headTextStyle,
align: 2,
breakStrategy: 1,
textDirection: 1,
};
let fontCollection = text.FontCollection.getGlobalInstance();
let headParagraphGraphBuilder = new text.ParagraphBuilder(headParagraphStyle, fontCollection);
let chaptersParagraphGraphBuilder = new text.ParagraphBuilder(chaptersParagraphStyle, fontCollection);
//更新文本样式
headParagraphGraphBuilder.pushStyle(headTextStyle);
chaptersParagraphGraphBuilder.pushStyle(chaptersTextStyle);
//添加标题
headParagraphGraphBuilder.addText(Constants.HEAD);
//添加段落
chaptersParagraphGraphBuilder.addText(Constants.CHAPTERS);
let headParagraph = headParagraphGraphBuilder.build();
let chaptersParagraph = chaptersParagraphGraphBuilder.build();
// 布局
headParagraph.layoutSync(1250);
chaptersParagraph.layoutSync(1250);
//绘制文本
headParagraph.paint(canvas,0, 0);
chaptersParagraph.paint(canvas,0, 120);
场景二:实现小说翻页
方案
- 通过String的splice方法截取当前也需要绘制的小说内容currentText。
- 通过 ParagraphGraphBuilder.addText添加文本,通过Paragraph.paint绘制文本。
- 给NodeContainer的父组件Row添加滑动手势,当左右滑动时String的splice方法更新currentText并触发重绘。
核心代码
aboutToAppear(): void {
currentText = Constants.TEXT.slice((pageIndex - 1) * Constants.PAGE_SIZE, pageIndex * Constants.PAGE_SIZE)
}
let textParagraphStyle: text.ParagraphStyle = {
textStyle: aTextStyle,
align: 1,
breakStrategy: 0,
textDirection: 1,
maxLines: 27
//wordBreak:text.WordBreak.NORMAL 文本断词类型
};
let textParagraphGraphBuilder = new text.ParagraphBuilder(textParagraphStyle, fontCollection);
//更新文本样式
textParagraphGraphBuilder.pushStyle(aTextStyle);
//添加文本
textParagraphGraphBuilder.addText(currentText);
let textParagraph = textParagraphGraphBuilder.build();
// 布局
textParagraph.layoutSync(1250);
//绘制文本
textParagraph.paint(canvas, 0, 0);
.onTouch((event: TouchEvent) => {
this.currentPositionX = event.touches[0].x
Logger.info('currentPositionX is: ' + this.currentPositionX)
})
.gesture(
// GestureGroup(GestureMode.Parallel,
SwipeGesture({ direction: SwipeDirection.Horizontal, speed: 20 })
.onAction(async (event: GestureEvent) => {
if (event) {
if (this.currentPositionX < 80) {
Logger.info('load lastNode ')
--pageIndex;
if (pageIndex <= 0) {
pageIndex = 1
}
currentText =
Constants.TEXT.slice((pageIndex - 1) * Constants.PAGE_SIZE, pageIndex * Constants.PAGE_SIZE)
const node = new CurTextRenderNode();
// 定义textNode的像素格式
node.frame = {
x: 0,
y: 0,
width: screenWidth,
height: screenHeight
};
this.myNodeController.clearNodes()
this.myNodeController.addNode(node)
return;
}
Logger.info('load nextNode ')
if (this.currentPositionX > 280) {
++pageIndex;
currentText = Constants.TEXT.slice((pageIndex - 1) * Constants.PAGE_SIZE, pageIndex * Constants.PAGE_SIZE)
const node = new CurTextRenderNode();
// 定义textNode的像素格式
node.frame = {
x: 0,
y: 0,
width: screenWidth,
height: screenHeight
};
this.myNodeController.clearNodes()
this.myNodeController.addNode(node)
}
}
})
)
场景三:背景渐变
当前渐变的接口在arkts没有开放,只有C侧接口。
方案
- 使用drawing_pen.h的OH_Drawing_PenCreate接口创建一个画笔实例cPen, 并设置抗锯齿、颜色、线宽等属性,画笔用于形状边框线的绘制。使用drawing_brush.h的OH_Drawing_BrushCreate接口创建一个画刷实例cBrush,并设置填充颜色, 画刷用于形状内部的填充。
- 通过OH_Drawing_ShaderEffectCreateLinearGradient()创建着色器,在两个指定点之间生成线性渐变。
- 通过OH_Drawing_BrushSetShaderEffect()为画刷设置着色器效果。
- 使用drawing_canvas.h的OH_Drawing_CanvasAttachPen和OH_Drawing_CanvasAttachBrush接口将画笔画刷的实例设置到画布实例中。
- OH_Drawing_CanvasDrawPath(cCanvas_, cPath_)绘制渐变色图形。
核心代码
float aX = width_ / 8;
float aY = height_ * 3 / 8;
float bX = width_ / 8;
float bY = height_ * 5 / 8;
float cX = width_ * 7 / 8;
float cY = height_ * 5 / 8;
float dX = width_ * 7 / 8;
float dY = height_ * 3 / 8;
// Create a path object used to build graphics
cPath_ = OH_Drawing_PathCreate();
startPt= OH_Drawing_PointCreate( aX, aY );
endPt=OH_Drawing_PointCreate( cX, cY );
// Set starting point
OH_Drawing_PathMoveTo(cPath_, aX, aY);
// Add a path to the target point
OH_Drawing_PathLineTo(cPath_, bX, bY);
OH_Drawing_PathLineTo(cPath_, cX, cY);
OH_Drawing_PathLineTo(cPath_, dX, dY);
OH_Drawing_PathClose(cPath_);
constexpr float penWidth = 10.0f; // pen width 10
// 创建一个画笔Pen对象,Pen对象用于形状的边框线绘制
cPen_ = OH_Drawing_PenCreate();
OH_Drawing_PenSetAntiAlias(cPen_, true);
OH_Drawing_PenSetColor(cPen_, OH_Drawing_ColorSetArgb(0xFF, 0xFF, 0x00, 0x00));
OH_Drawing_PenSetWidth(cPen_, penWidth);
OH_Drawing_PenSetJoin(cPen_, LINE_ROUND_JOIN);
// 将Pen画笔设置到canvas中
OH_Drawing_CanvasAttachPen(cCanvas_, cPen_);
uint32_t color[] = {0xffff0000, 0xff00ff00};
float pos[] = {0., 1.0};
// 创建并设置Brush的渐变效果
uint32_t size = 2; // shader size 2
shaderEffect_=OH_Drawing_ShaderEffectCreateLinearGradient(startPt,endPt,color,pos,size, OH_Drawing_TileMode::CLAMP);
// 创建一个画刷Brush对象,Brush对象用于形状的填充
cBrush_ = OH_Drawing_BrushCreate();
OH_Drawing_BrushSetShaderEffect(cBrush_, shaderEffect_);
OH_Drawing_BrushSetColor(cBrush_, OH_Drawing_ColorSetArgb(0xFF, 0xFF, 0xFF, 0x00));
// 将Brush画刷设置到canvas中
OH_Drawing_CanvasAttachBrush(cCanvas_, cBrush_);
场景四:坐标获取,局部刷新
Canvas的clipRect方法用于裁剪画布,将画布限制在指定的矩形区域内。裁剪后,只有在该区域内的绘制内容才会被显示出来,超出该区域的内容将被隐藏。裁剪画布的作用是可以控制绘制的范围,只绘制在指定区域内的内容,可以实现一些特殊的绘制效果。
方案
- 给RenderNode的父组件Row ,添加 onTouch事件,在onTouch事件里面获取当前触摸点坐标。
- 在 onTouch事件中通过clipRect绘制刷新局部区域。
核心代码
.onTouch((event: TouchEvent) => {
currentPositionX = vp2px(event.touches[0].x)
currentPositionY = vp2px(event.touches[0].y)
console.info('currentPositionX1 is: ' + currentPositionX + 'currentPositionY is : ' + currentPositionY);
})
绘制五角心
// 创建一个画笔Pen对象,Pen对象用于形状的边框线绘制
let pen = new drawing.Pen();
pen.setAntiAlias(true);
let pen_color: common2D.Color = {
alpha: 0xFF,
red: 0xFF,
green: 0x00,
blue: 0x00
};
pen.setColor(pen_color);
pen.setStrokeWidth(10.0);
// 将Pen画笔设置到canvas中
canvas.attachPen(pen);
// 创建一个画刷Brush对象,Brush对象用于形状的填充
let brush = new drawing.Brush();
let brush_color: common2D.Color = {
alpha: 0xFF,
red: 0x00,
green: 0x00,
blue: 0xFF
};
brush.setColor(brush_color);
// 将Brush画刷设置到canvas中
canvas.attachBrush(brush);
// 绘制path
canvas.drawPath(path);
// 绘制完成后清空画布上已设置的画笔和画刷
canvas.detachBrush();
canvas.detachPen();
局部刷新,改变五角心局部区域颜色
// 创建一个画笔Pen对象,Pen对象用于形状的边框线绘制
let pen = new drawing.Pen();
pen.setAntiAlias(true);
let pen_color: common2D.Color = {
alpha: 0xFF,
red: 0xFF,
green: 0x00,
blue: 0x00
};
pen.setColor(pen_color);
pen.setStrokeWidth(10.0);
// 将Pen画笔设置到canvas中
canvas.attachPen(pen);
// 创建一个画刷Brush对象,Brush对象用于形状的填充
let brush = new drawing.Brush();
//设置画刷为绿色
let brush_color: common2D.Color = {
alpha: 0xFF,
red: 0x00,
green: 0xFF,
blue: 0x00
};
brush.setColor(brush_color);
// 将Brush画刷设置到canvas中
canvas.attachBrush(brush);
//截取绘图生效区域
canvas.clipRect(region, drawing.ClipOp.INTERSECT, true);
// 在截取的绘图生效区域绘制
canvas.drawPath(path);
// 绘制完成后清空画布上已设置的画笔和画刷
canvas.detachBrush();
canvas.detachPen();
效果好漂亮,爱了
很实用