This article is the fourth in the “Flutter Framework in Less complicated” series. The main purpose of this article is to prepare for the introduction of RenderObject later. In this paper, PaintingContext is analyzed in detail, mainly including how PaintingContext is drawn with RenderObject in Rendering Pipeline, and some basic concepts are briefly introduced (e.g. Canvas, Picture, PictureRecorder, SceneBuilder, Scene, etc.)

This article is also published on my personal blog

This series of articles explores the core concepts and processes of the Flutter Framework step by step, including:

  • “Widgets of the Flutter Framework”
  • “BuildOwner of the Flutter Framework”
  • Elements of the Flutter Framework
  • “PaintingContext of the Flutter Framework”
  • “Layer of the Flutter Framework”
  • PipelineOwner of the Flutter Framework
  • RenderObejct of the Flutter Framework
  • “Binding of the Flutter Framework”
  • “The Rendering Pipeline of the Flutter Framework”
  • “Custom Widgets from the Flutter Framework”

Overview


“Widget” — “Element” — “RenderObject” can be called the “Three Musketeers” of the Flutter Framework. Among the three, RenderObject is the most core and complex, involving Layout, Paint and other core processes. In order to understand RenderObject better and more smoothly, before the formal introduction, we need to do some preparatory work. PaintingContext introduced in this article plays an important role in the drawing process of RenderObject.

The name “Painting Context” says something: Painting Context, the simplest way to think about it is to provide the place or Context for the Paint operation. Its main responsibilities include:

  • Introduce new Layer(mainly based on Repaint Boundary and need Compositing) in the rendering process as needed;
  • Maintaining the “Layer Tree” generates a Layer Sub Tree for each PaintingContext instance;
  • Management Canvas abstracts and encapsulates the low-level details.

As shown above:

  • PaintingContextInherited fromClipContext.ClipContextClipping is an abstract class that provides several helper methods related to clipping.
  • PictureLayer _currentLayer,ui.PictureRecorder _recorderAs well asCanvas _canvasFor specific drawing operations;
  • ContainerLayer _containerLayer, the root node of “Layer Subtree”, byPaintingContextThe constructor is passed in, which is usually passed inRenderObject._layer.

Renderobjects and layers have a many-to-one relationship, that is, multiple RenderObjects are drawn on a Layer.

Basic concept


This section briefly introduces some of the basic concepts mentioned in the previous section.

Canvas

Canvas is a bridge between the Engine(C++) layer and the Framework(Dart) layer. The real functionality is implemented in the Engine layer.

Picture, PictureRecorder, SceneBuilder, and SceneBuilder are all Bridges from the Engine(C++) layer to the Framework(Dart) layer.

Canvas exposes basic interfaces related to drawing to the Framework layer, such as Draw *, Clip *, Transform and Scale, etc. RenderObject uses these basic interfaces to complete drawing tasks.

All operations performed through this interface are recorded by PictureRecorder.

Canvas(PictureRecorder recorder, [ Rect cullRect ]){}
Copy the code

As shown above, you need to specify a PictureRecorder when the Canvas is initialized to record all the “graphical operations”.

In addition to normal draw operations, Canvas also supports Transformation Matrix and Clip region, which will apply to all subsequent draw operations on the Canvas. Here are some ways to get a feel for it:

void scale(double sx, [double sy]);
void rotate(double radians) native;
void transform(Float64List matrix4);

void clipRect(Rect rect, { ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true });
void clipPath(Path path, {bool doAntiAlias = true});

void drawColor(Color color, BlendMode blendMode);
void drawLine(Offset p1, Offset p2, Paint paint);
void drawRect(Rect rect, Paint paint);
void drawCircle(Offset c, double radius, Paint paint);
void drawImage(Image image, Offset p, Paint paint);
void drawParagraph(Paragraph paragraph, Offset offset);
Copy the code

Picture

It is essentially a collection of “graphical operations” that are transparent to the Framework layer. Future toImage(int width, int height), toImage method can record all operations rasterized to generate an Image object.

PictureRecorder

Its main function is to record the “graphical operations” performed on the Canvas, and finally generate a Picture through Picture#endRecording.

Scene

Also transparent to the Framework layer and is the result of a composite of pictures and textures.

An opaque object representing a composited scene.

When UI frame is refreshed, the Flutter UI in Rendering Pipeline generates Scene after build, layout, paint and other steps. Then, the Scene is sent to the Engine layer through window-render, and finally displayed on the screen after rasterization by GPU.

SceneBuilder

Use to combine layers, Picture, and Texture into Scene.

 void addPicture(Offset offset, Picture picture, { bool isComplexHint = false.bool willChangeHint = false });
 void addTexture(int textureId, { Offset offset = Offset.zero, double width = 0.0.double height = 0.0 , bool freeze = false});
Copy the code

The addPicture and addTexture allow you to import the Picture and Texture you want to synthesize.

SceneBuilder also maintains a stack of graphics operations:

pushTransform
pushOffset
pushClipRect
...
pop
Copy the code

These operations are mainly used for OffsetLayer and ClipRectLayer.

Do you feel very abstract, dizzy! Let’s string them together with a little example to get a real feel for it.

Small example

void main() {
  PictureRecorder recorder = PictureRecorder();
  // To initialize the Canvas, pass in an instance of PictureRecorder
  // Record all actions that occur on the canvas
  //
  Canvas canvas = Canvas(recorder);

  Paint circlePaint= Paint();
  circlePaint.color = Colors.blueAccent;

  // Call Canvas's drawing interface and draw a circle
  //
  canvas.drawCircle(Offset(400.400), 300, circlePaint);

  // 绘制结束,生成Picture
  //
  Picture picture = recorder.endRecording();

  SceneBuilder sceneBuilder = SceneBuilder();
  sceneBuilder.pushOffset(0.0);
  // Send the picture to SceneBuilder
  //
  sceneBuilder.addPicture(Offset(0.0), picture);
  sceneBuilder.pop();

  / / generated Scene
  //
  Scene scene = sceneBuilder.build();

  window.onDrawFrame = () {
    // Send the scene to the Engine layer for rendering
    //
    window.render(scene);
  };
  window.scheduleFrame();
}
Copy the code

By directly manipulating the Canvas, we have drawn ⭕️ on the screen.

Just for demonstration purposes, you don’t need to work directly with these basic apis in everyday development.

Drawing process


The drawing process described in this section is limited to the PaintingContext area; a more complete process will be analyzed when we introduce RenderObject.

What is the relationship between PaintingContext and RenderObject? From an “interclass relationship” perspective, they aredependenciesRenderObject depends on PaintingContext — PaintingContext appears as an argument in the RenderObject’s draw method. In other words, the PaintingContext is a one-time event. Each time the Paint is executed, a corresponding PaintingContext is generated, and its life cycle ends when the Paint is finished. PaintingContext’s role in drawing a RenderObject is shown below:

  • When the UI Frame refreshes, passesRendererBinding#drawFrame->PipelineOwner#flushPaintThe triggerRenderObject#paint;
  • RenderObject#paintcallPaintingContext.canvasProvided graphical operation interface (draw*,clip*,transform) complete the drawing task;
  • The above drawing operation is recorded by PictureRecorder, which generates a picture at the end of the drawing and is added to the PictureLayer (_currentLayer);
  • Then, the RenderObject passesPaintingContext#paintChildDraw a child node recursively (child RenderObject, if any);
  • When drawing a child node, a different strategy is used depending on whether the child node is “Repaint Boundary” :
    • Is “Repaint Boundary” – a new PaintingContext is generated for the child node so that the child node can be drawn independently and the result is a “Layer subTree” which is appended to the “Layer Tree” generated by the parent node;
    • Not “Repaint Boundary” – the child node is drawn directly over the currentPaintingContext.canvasRenderObject and Layer have a many-to-one relationship.
  • At the end of the whole drawing process, a “Layer Tree” is obtained, and then the Scene is generated through the SceneBuilder, and thenwindow.renderIt is fed into the Engine layer, and finally rasterized by the GPU and displayed on the screen.

The concept of Repaint Boundary will be highlighted in the presentation of RenderObject.

Several methods play a key role in the above process:

  Canvas get canvas {
    if (_canvas == null)
      _startRecording();
    return _canvas;
  }

  void _startRecording() {
    // The layer is added to the Picture generated by the action on the current Canvas
    // 
    _currentLayer = PictureLayer(estimatedBounds);
    _recorder = ui.PictureRecorder();
    
    // Initialize the Canvas, passing _recorder
    //
    _canvas = Canvas(_recorder);
    
    // Insert _currentLayer into a subtree with _containerLayer as the root node
    //
    _containerLayer.append(_currentLayer);
  }

  void stopRecordingIfNeeded() {
    // When the recording stops, add the result picture to _currentLayer
    //
    _currentLayer.picture = _recorder.endRecording();
    
    / / attention!
    // At this point, _currentLayer, _recorder, _canvas are released,
    // After that, if you want to draw through the current PaintingContext, new _currentLayer, _Recorder, _Canvas will be generated
    // The _canvas may change during the lifetime of the PaintingContext
    //
    _currentLayer = null;
    _recorder = null;
    _canvas = null;
  }
Copy the code

Compositing


Compositing is a part of the Rendering Pipeline that indicates whether new layers are generated to achieve certain graphical effects.

RenderObject. Whether you need needCompositing said the RenderObject synthesis, namely, whether in the paint method need to generate a new Layer. More detailed information will be analyzed in the presentation of RenderObject.

Normally, the RenderObject will process the Compositing using PaintingContext#push* :

  void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
    / / attention!
    // Terminate the existing draw operation before the Append sub layer
    // See above for stopRecordingIfNeeded operations performed
    //
    stopRecordingIfNeeded();
    appendLayer(childLayer);
    
    // Create a new PaintingContext for the childLayer to draw independently
    //
    final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
    painter(childContext, offset);
    childContext.stopRecordingIfNeeded();
  }

  PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
    return PaintingContext(childLayer, bounds);
  }

  / / needsCompositing parameters generally comes from RenderObject needCompositing
  //
  ClipRectLayer pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge, ClipRectLayer oldLayer }) {
    final Rect offsetClipRect = clipRect.shift(offset);
    if (needsCompositing) {
      // Create a new Layer when composition is needed
      //
      finalClipRectLayer layer = oldLayer ?? ClipRectLayer(); layer .. clipRect = offsetClipRect .. clipBehavior = clipBehavior;// Add the new layer to the Layer tree and finish drawing on it
      //
      pushLayer(layer, painter, offset, childPaintBounds: offsetClipRect);
      return layer;
    } else {
      // Otherwise, crop and draw on the current Canvas
      //
      clipRectAndPaint(offsetClipRect, clipBehavior, offsetClipRect, () => painter(this, offset));
      return null; }}Copy the code

As shown above, pushClipRect creates a new Layer when needsCompositing is true and cuts and draws on it; otherwise, cuts and paints on the current Canvas.

example

Let’s string the above together with a simple example:

void main() {
  ContainerLayer containerLayer = ContainerLayer();
  PaintingContext paintingContext = PaintingContext(containerLayer, Rect.zero);

  Paint circle1Paint= Paint();
  circle1Paint.color = Colors.blue;

  / / comment 1
  // paintingContext.canvas.save();
  
  // Crop the canvas
  //
  paintingContext.canvas.clipRect(Rect.fromCenter(center: Offset(400.400), width: 280, height: 600));

  // Draw ⭕️ on the cropped canvas
  //
  paintingContext.canvas.drawCircle(Offset(400.400), 300, circle1Paint);

  / / comment 2
  // paintingContext.canvas.restore();

  void _painter(PaintingContext context, Offset offset) {
    Paint circle2Paint = Paint();
    circle2Paint.color = Colors.red;
    context.canvas.drawCircle(Offset(400.400), 250, circle2Paint);
  }

  // Perform clipping again with pushClipRect
  // Note that the needsCompositing parameter is true
  //
  paintingContext.pushClipRect(true, Offset.zero, Rect.fromCenter(center: Offset(500.400), width: 200, height: 200), _painter,);

  Paint circle3Paint= Paint();
  circle3Paint.color = Colors.yellow;

  // Draw ⭕️ again
  //
  paintingContext.canvas.drawCircle(Offset(400.800), 300, circle3Paint);
  paintingContext.stopRecordingIfNeeded();

  // To reduce space, the code associated with generating the Scene has been omitted
}
Copy the code

The drawing result is shown in the figure below:If the above code in the callpaintingContext.pushClipRectWhen,needsCompositingParameters forfalse, the results are as follows:So, inneedsCompositingParameters forfalse, how to achieve the effect shown in Figure 1? Simply remove the comments at 1 or 2 points in the code. I’m not going to analyze the process, but if you’re interested, you can analyze it yourself.

conclusion

PaintingContext plays an important role in assisting rendering of RenderObject, such as: management of Layer Tree, processing of Repaint Boundary and need Compositing, encapsulation of basic API, etc. Knowing this will help you understand RenderObject later.

The resources

Flutter Internals