引擎渲染接口

前面已经剖析了框架从启动到渲染的过程,这里我们抛开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”

20190301102949549

Copyright © Arcticfox 2020 all right reserved,powered by Gitbook文档修订于: 2022-05-01 12:00:54

results matching ""

    No results matching ""