引擎渲染接口
前面已经剖析了框架从启动到渲染的过程,这里我们抛开Flutter 整个Widget控件体系,直接使用引擎底层渲染接口,来试验一下图形的绘制和渲染。
首先按照Layer的概念,写一个例子,关于Layer 可其查看UML大图

其中 PictureLayout 是主要的图像绘制层;TextureLayer 则用于外界纹理的实现,通过它可以实现如相机、视频播放、OpenGL等相关操作;ContainerLayer是有一个具有子项列表的复合层,它及其子类带有 append 方法,可将给定的图层添加到这个图层的子列表的末尾。但其本身是不具备“描绘”控件的能力,如要呈现画面需要和 PictureLayer 结合。而非 ContainerLayer 一系的Layer类一般不具备子节点。
import 'package:flutter/material.dart' show Colors;
import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';
void main() {
/// 每当引擎希望我们产生新的一帧时,就会回调onBeginFrame
ui.window.onBeginFrame = beginFrame;
/// 在这里,通过要求引擎安排一个新的帧来开始整个过程。
/// 当真正要制作帧的时候,引擎最终会调用onBeginFrame。
ui.window.scheduleFrame();
}
void beginFrame(Duration timeStamp) {
// 设备像素比给出了设备屏幕上的像素大小与 "normal"大小像素的大致比例。
// 我们通常以逻辑像素为单位,然后在绘制到屏幕上之前,按设备像素比进行缩放。
final double devicePixelRatio = ui.window.devicePixelRatio;
final ui.Size logicalSize = ui.window.physicalSize / devicePixelRatio;
// 创建一个PictureRecorder来记录我们要在画布中输入的命令
// PictureRecorder最终会生成一个Picture,它是这些命令的不可变记录
final ui.PictureRecorder recorder = ui.PictureRecorder();
// 接下来,从记录器中创建一个画布。canvas接口是以Skia的SkCanvas建模的
final ui.Canvas canvas = ui.Canvas(recorder);
// 这个变换让我们可以用 "逻辑 "像素进行绘制,这些像素通过这个缩放操作转换为设备的物理像素
canvas.scale(devicePixelRatio, devicePixelRatio);
/// 绘制背景色
canvas.drawColor(Colors.grey, ui.BlendMode.src);
///画一个100x100的白色矩形,并水平居中
canvas.drawRect(
ui.Rect.fromLTWH((logicalSize.width-100)/2,0,100,100),
ui.Paint()..color = Colors.white);
/// 绘制文字
final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
ui.ParagraphStyle(textDirection: ui.TextDirection.ltr),
)
..pushStyle(ui.TextStyle(color: Colors.yellow,fontSize: 26))
..addText('Hello, world');
final ui.Paragraph paragraph = paragraphBuilder.build()
..layout(ui.ParagraphConstraints(width: logicalSize.width));
/// 文本居中绘制
canvas.drawParagraph(paragraph, ui.Offset(
(logicalSize.width - paragraph.maxIntrinsicWidth) / 2.0,
(logicalSize.height - paragraph.height) / 2.0,
));
/// 创建根 Layer
OffsetLayer rootLayer = OffsetLayer();
/// 创建用于绘制的Layer
PictureLayer pictureLayer = PictureLayer(ui.Rect.zero);
rootLayer.append(pictureLayer);
// 发出绘画命令后,结束记录并返回Picture,这是我们发出的命令的不变记录
// 你可以将Picture绘制到另一个画布中,或将其作为合成场景的一部分
pictureLayer.picture = recorder.endRecording();
// 使用SceneBuilder建立一个简单的场景
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
rootLayer.addToScene(sceneBuilder);
ui.window.render(sceneBuilder.build());
}
以上示例更符合框架本身的实现方式,实际上我们可以完全抛开Layer概念,直接使用底层接口完成绘制
void beginFrame(Duration timeStamp) {
/// ......省略相同代码,参见上例......
/// 文本居中绘制
canvas.drawParagraph(paragraph, ui.Offset(
(logicalSize.width - paragraph.maxIntrinsicWidth) / 2.0,
(logicalSize.height - paragraph.height) / 2.0,
));
///结束绘制
final ui.Picture picture = recorder.endRecording();
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder()
..addPicture(ui.Offset.zero, picture)
..pop();
ui.window.render(sceneBuilder.build());
}
再来看一下如何处理触摸事件
import 'dart:ui' as ui;
ui.Color color;
void main() {
color = const ui.Color(0xFF00FF00);
ui.window.onBeginFrame = beginFrame;
// 每当引擎更新了有关指向我们应用程序的指针的信息时,就会调用onPointerDataPacket
ui.window.onPointerDataPacket = handlePointerDataPacket;
ui.window.scheduleFrame();
}
void handlePointerDataPacket(ui.PointerDataPacket packet) {
/// packet 中包含了许多指针运动,对这些指针进行迭代和处理。
for (ui.PointerData datum in packet.data) {
if (datum.change == ui.PointerChange.down) {
// 如果指针向下,就把圆圈的颜色改为蓝色。
color = const ui.Color(0xFF0000FF);
// 要求引擎安排一个帧。当真正到了制作帧的时候,引擎会调用onBeginFrame。
ui.window.scheduleFrame();
} else if (datum.change == ui.PointerChange.up) {
// 如果指针向上,把圆圈的颜色改为绿色,然后安排一个框架。
// 多次调用 scheduleFrame 是无害的,因为在引擎调用 onBeginFrame 之前,
// 会忽略多余的请求,而 onBeginFrame 是一帧与另一帧之间的边界。
color = const ui.Color(0xFF00FF00);
ui.window.scheduleFrame();
}
}
}
void beginFrame(Duration timeStamp) {
// 用逻辑像素表示的全屏矩形区域
final ui.Rect paintBounds = ui.Offset.zero & (ui.window.physicalSize/ui.window.devicePixelRatio);
/// ******************************************
/// *** 首先,使用绘画命令记录一个Picture ***
/// ******************************************
final ui.PictureRecorder recorder = ui.PictureRecorder();
// paintBounds为canvas建立了一个 "剪裁矩形",
// 这使实现可以丢弃完全在此矩形外部的所有命令
final ui.Canvas canvas = ui.Canvas(recorder, paintBounds);
final double devicePixelRatio = ui.window.devicePixelRatio;
canvas.scale(devicePixelRatio, devicePixelRatio);
// 在屏幕中央绘制一个圆圈。
final ui.Size size = paintBounds.size;
canvas.drawCircle(
size.center(ui.Offset.zero),
size.shortestSide * 0.45,
ui.Paint()..color = color,
);
final ui.Picture picture = recorder.endRecording();
/// ***************************************
/// *** 其次,将该Picture包含在场景图中 ***
/// ***************************************
// 使用SceneBuilder建立一个简单的场景图
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder()
..addPicture(ui.Offset.zero, picture)
..pop();
// 当录制完场景后,调用build()来获取录制的场景的不可变记录
final ui.Scene scene = sceneBuilder.build();
/// ***************************************
/// *** 最后,指示引擎渲染该场景图 ***
/// ***************************************
ui.window.render(scene);
}
关于更多调用引擎接口渲染的示例,参见官方的 examples
公众号“编程之路从0到1”
