Image rendering principle of Flutter

Widget, Element, RenderObject

Iii. Update process of Flutter UI

Iv. Build process analysis

5. Process analysis

6. Paint (1)

7. Paint (2)

Viii. Composite process analysis

1. Customize the layout

Click a prompt box according to the location of the click, for example

There are four main situations for the position of the prompt box:

The dots in the diagram above (the middle of the screen) are the reference points

Horizontal direction:

Displayed to the left of the reference: The middle point of the reference is located to the right of the dot, i.e. the prompt box is displayed to the left of the reference

Displayed to the right of the reference: The middle point of the reference is located to the left of the dot, i.e. the prompt box is displayed to the right of the reference

Vertical direction:

Display below the reference: The middle point of the reference is located above the dot, that is, the prompt box is displayed below the reference

Display above the reference: The middle point of the reference is located below the dot, that is, the prompt box is displayed above the reference

Step (1) Obtain the position of the reference when the click event is triggered

ShowTip (BuildContext context) async / / access click source final RenderBox box. = customPopupKey currentContext. FindRenderObject (); final Offset target = box.localToGlobal(box.size.center(Offset.zero)); final RenderBox overlay = Overlay.of(context).context.findRenderObject(); }Copy the code

(2) Judge the position of the prompt box, left, right, top, bottom

PopupDirection popupDirection; 

  if(preferVertical) {// Indicates whether the display is verticalif (target.dy > overlay.size.center(Offset.zero).dy) {
      popupDirection = PopupDirection.up;
    } else{ popupDirection = PopupDirection.down; }}else{// horizontal displayif (target.dx < overlay.size.center(Offset.zero).dx) {
      popupDirection = PopupDirection.right;
    } else{ popupDirection = PopupDirection.left; }}Copy the code

(3) Rewrite the performLayout function to return the size of the corresponding node in the performLayout function, return the renderObejct offset, that is, the location information, This example uses the CustomSingleChildLayout provided by Flutter to customize the layout

  delegate: _CustomSingleChildDelegate(
      target: target,
      targetBoxSize: box.size,
      offsetDistance: offsetDistance,
      preferVertical: preferVertical,
      popUpDirection: popupDirection),
  child: Container(
    padding: contentPadding,
    decoration: ShapeDecoration(
      color: bgColor,
      shape: CustomShapeBorder(
          popupDirection: popupDirection,
          targetCenter: target,
          borderRadius: _defaultBorderRadius,
          arrowBaseWidth: arrowBaseWidth,
          arrowTipDistance: arrowTipDistance,
          borderColor: borderColor,
          borderWidth: borderWidth),
    ),
    child: Text(
      message,
      style: TextStyle(
          fontSize: Adapt.px(textSize),
          color: textColor,
          decoration: TextDecoration.none),
    ),
  ),
)
Copy the code

RenderObject to CustomSingleChildLayout overwrites performLayout

void performLayout() {
  size = _getSize(constraints);
  if(child ! = null) { final BoxConstraints childConstraints = delegate.getConstraintsForChild(constraints); assert(childConstraints.debugAssertIsValid(isAppliedConstraint:true));
    child.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = delegate.getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child.size);
  }
}
Copy the code

Its Settings node is offset by invoking the delegate. GetPositionForChild methods, the delegate is by the caller custom, such as incoming _CustomSingleChildDelegate demo, in this class, We rewrite the getPositionForChild method to lay out the node locations according to our needs

(5) _CustomSingleChildDelegate

class _CustomSingleChildDelegate extends SingleChildLayoutDelegate {

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    returncustomPositionDependentBox( size: size, childSize: childSize, target: target, offsetDistance: offsetDistance, preferVertical: preferVertical, ); } Offset customPositionDependentBox({ @required Size size, @required Size childSize, @required Offset target, @required bool preferVertical, double offsetDistance = 0.0, double margin = 30.0,}) {// VERTICAL DIRECTION double x; double y; // Display perpendicular to the click positionif (preferVertical) {
      final bool fitsBelow = popUpDirection == PopupDirection.down;

      ifY = min(target.dy + 5, size.height-margin); }elseY = Max (target.dy-childsize. Height-5, margin); } // horizontal processingif(size.width - childsize.width) {x = (size.width - childsize.width) / 2.0; }else {
        final double normalizedTargetX =
            target.dx.clamp(margin, size.width - margin);
        final double edge = margin + childSize.width / 2.0;
        if (normalizedTargetX < edge) {
          x = margin;
        } else if (normalizedTargetX > size.width - edge) {
          x = size.width - margin - childSize.width;
        } else{x = normalizedTargetX-childsize.width / 2.0; }}}elseFinal bool fitsLeft = popUpDirection == popupdirection. left;if(fitsLeft) {// left x = min(target.dx-childsize. width - targetboxSize. width / 2-10, size.width - margin); }else{// right x = Max (target.dx + targetboxsize.width / 2 + 10, margin); } // Vertical processing for horizontal displayif(size. height-margin * 2.0 < childsize. height) {y = (size. height-margin) / 2.0; }else{ final double normalizedTargetY = target.dy.clamp(margin, size.height - margin); Final double edge = margin + childSize. Height / 2.0;if (normalizedTargetY < edge) {
          y = margin;
        } else if (normalizedTargetY > size.height - edge) {
          y = size.height - margin - childSize.height;
        } else{y = normalizedTargety.height / 2.0; }}}returnOffset(x, y); }}Copy the code

2, custom drawing

In the above example, we have completed the custom layout, but this example has a prompt box with a triangle, and the Angle of the triangle can be different depending on the position of the prompt box, it can be up, down, left or right, this triangle can be customized according to the needs.

(1) By redrawing the border of the prompt box

Container(
  padding: contentPadding,
  decoration: ShapeDecoration(
    color: bgColor,
    shape: CustomShapeBorder(
        popupDirection: popupDirection,
        targetCenter: target,
        borderRadius: _defaultBorderRadius,
        arrowBaseWidth: arrowBaseWidth,
        arrowTipDistance: arrowTipDistance,
        borderColor: borderColor,
        borderWidth: borderWidth),
  ),
  child: Text(
    message,
    style: TextStyle(
        fontSize: Adapt.px(textSize),
        color: textColor,
        decoration: TextDecoration.none),
  ),
)
Copy the code

(2) Customize the border of the prompt box

*/ class CustomShapeBorder extends ShapeBorder {final Offset targetCenter; final double arrowBaseWidth; final double arrowTipDistance; final double borderRadius; final Color borderColor; final double borderWidth; final PopupDirection popupDirection; CustomShapeBorder( {this.popupDirection, this.targetCenter, this.borderRadius, this.arrowBaseWidth, this.arrowTipDistance, this.borderColor, this.borderWidth}); @override EdgeInsetsGeometry get dimensions => new EdgeInsets. All (10.0); @override Path getInnerPath(Rect rect, {TextDirection textDirection}) {returnnew Path() .. fillType = PathFillType.evenOdd .. addPath(getOuterPath(rect), Offset.zero); } @override // Create a Path getOuterPath(Rect Rect, {TextDirection TextDirection}) {double topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius; Path _getLeftTopPath(Rect rect) {returnnew Path() .. moveTo(rect.left, rect.bottom - bottomLeftRadius) .. lineTo(rect.left, rect.top + topLeftRadius) .. ArcToPoint (Offset(rect.left + topLeftRadius, rect.top), // Draw radius: new radius. Circular (topLeftRadius)).. lineTo(rect.right - topRightRadius, rect.top) .. ArcToPoint (Offset(rect.right, rect.top + topRightRadius), // circle radius: new radius. Circular (topRightRadius), clockwise:true);
    }

    Path _getBottomRightPath(Rect rect) {
      returnnew Path() .. moveTo(rect.left + bottomLeftRadius, rect.bottom) .. lineTo(rect.right - bottomRightRadius, rect.bottom) .. arcToPoint(Offset(rect.right, rect.bottom - bottomRightRadius), radius: new Radius.circular(bottomRightRadius), clockwise:false).. lineTo(rect.right, rect.top + topRightRadius) .. arcToPoint(Offset(rect.right - topRightRadius, rect.top), radius: new Radius.circular(topRightRadius), clockwise:false);
    }

    topLeftRadius = borderRadius;
    topRightRadius = borderRadius;
    bottomLeftRadius = borderRadius;
    bottomRightRadius = borderRadius;

    switch (popupDirection) {
      //

      case PopupDirection.down:
        return_getBottomRightPath(rect) .. lineTo( min( max(targetCenter.dx + arrowBaseWidth / 2, rect.left + borderRadius + arrowBaseWidth), rect.right - topRightRadius), rect.top) .. LineTo (targetcenter.dx, rect.top-arrowTipDistance) // Down arrow.. lineTo( max( min(targetCenter.dx - arrowBaseWidth / 2, rect.right - topLeftRadius - arrowBaseWidth), Rect. left + topLeftRadius), rect.top) // // down arrow.. lineTo(rect.left + topLeftRadius, rect.top) .. arcToPoint(Offset(rect.left, rect.top + topLeftRadius), radius: new Radius.circular(topLeftRadius), clockwise:false).. lineTo(rect.left, rect.bottom - bottomLeftRadius) .. arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom), radius: new Radius.circular(bottomLeftRadius), clockwise:false);

      case PopupDirection.up:
        return_getLeftTopPath(rect) .. lineTo(rect.right, rect.bottom - bottomRightRadius) .. arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom), radius: new Radius.circular(bottomRightRadius), clockwise:true).. lineTo( min( max(targetCenter.dx + arrowBaseWidth / 2, rect.left + bottomLeftRadius + arrowBaseWidth), Rect. Right - bottom right - bottom) // up arrow.. lineTo(targetCenter.dx, rect.bottom + arrowTipDistance) .. lineTo( max( min(targetCenter.dx - arrowBaseWidth / 2, rect.right - bottomRightRadius - arrowBaseWidth), rect.left + bottomLeftRadius), rect.bottom) .. lineTo(rect.left + bottomLeftRadius, rect.bottom) .. arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius), radius: new Radius.circular(bottomLeftRadius), clockwise:true).. lineTo(rect.left, rect.top + topLeftRadius) .. arcToPoint(Offset(rect.left + topLeftRadius, rect.top), radius: new Radius.circular(topLeftRadius), clockwise:true);

      case PopupDirection.left:
        return_getLeftTopPath(rect) .. lineTo( rect.right, max( min(targetCenter.dy - arrowBaseWidth / 2, rect.bottom - bottomRightRadius - arrowBaseWidth), rect.top + topRightRadius)) .. LineTo (rect.right + arrowTipDistance, targetCenter.dy) // left arrow.. lineTo( rect.right, min(targetCenter.dy + arrowBaseWidth / 2, rect.bottom - bottomRightRadius)) .. lineTo(rect.right, rect.bottom - borderRadius) .. arcToPoint(Offset(rect.right - bottomRightRadius, rect.bottom), radius: new Radius.circular(bottomRightRadius), clockwise:true).. lineTo(rect.left + bottomLeftRadius, rect.bottom) .. arcToPoint(Offset(rect.left, rect.bottom - bottomLeftRadius), radius: new Radius.circular(bottomLeftRadius), clockwise:true);

      case PopupDirection.right:
        return_getBottomRightPath(rect) .. lineTo(rect.left + topLeftRadius, rect.top) .. arcToPoint(Offset(rect.left, rect.top + topLeftRadius), radius: new Radius.circular(topLeftRadius), clockwise:false).. lineTo( rect.left, max( min(targetCenter.dy - arrowBaseWidth / 2, rect.bottom - bottomLeftRadius - arrowBaseWidth), Rect. top + topLeftRadius)) // Arrow to the right.. lineTo(rect.left - arrowTipDistance, targetCenter.dy) .. lineTo( rect.left, min(targetCenter.dy + arrowBaseWidth / 2, rect.bottom - bottomLeftRadius)) .. lineTo(rect.left, rect.bottom - bottomLeftRadius) .. arcToPoint(Offset(rect.left + bottomLeftRadius, rect.bottom), radius: new Radius.circular(bottomLeftRadius), clockwise:false); default: throw AssertionError(popupDirection); } } @override void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) { Paint paint = new Paint() .. color = borderColor .. style = PaintingStyle.stroke .. strokeWidth = borderWidth; canvas.drawPath(getOuterPath(rect), paint); } @override ShapeBorder scale(double t) {returnnew CustomShapeBorder( popupDirection: this.popupDirection, targetCenter: this.targetCenter, borderRadius: this.borderRadius, arrowBaseWidth: this.arrowBaseWidth, arrowTipDistance: this.arrowTipDistance, borderColor: this.borderColor, borderWidth: this.borderWidth); }}Copy the code

Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > CustomSingleChildLayout

3. Performance optimization

(1) devTool tools

If you want to do a good job, you must do a good job. You can use devTool to open the timeline to view the relevant rendering situation

Or you can bring up the Flutter Inspector in Android Studio

(2) Set the following property values in the main method

void main() {
  debugProfileBuildsEnabled = true; / / check needs to redraw the widget debugProfilePaintsEnabled =true; / / check needs to redraw renderObject debugPaintLayerBordersEnabled =true; / / check needs to redraw debugRepaintRainbowEnabled = layer informationtrue;
  runApp(MyApp());
}
Copy the code

(3) Reduce the rebuild scope

class _MyHomePageState extends State<MyHomePage> {
  String name = ' ';
  int count = 0;
  @override
  void initState() {
    super.initState();
    Timer.periodic(
        Duration(milliseconds: 1000),
            (timer) => {
          setState(() { count++; })}); } @override Widget build(BuildContext context) {return Scaffold(
        body: Container(
      child: Row(
        children: <Widget>[
          Container(
            child: Row(
              children: <Widget>[Text('Hahaha 1'), Text('Hahaha 3')],
            ),
          ),
          Text(${count} ${count})],))); }}Copy the code

Before optimization:

The widget is rebuilt each time from the root Scaffold, but in this demo only one text text is updated in real time

Optimized: Separate the widgets that need to be updated in real time into a component

class TextCom extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _TextComState();
  }
}

class _TextComState extends State<TextCom> {
  int count = 0;
  @override
  void initState() {
    super.initState();
    Timer.periodic(
        Duration(milliseconds: 1000),
        (timer) => {
              setState(() { count++; })}); } @override Widget build(BuildContext context) {return Text(${count} ${count}); }}Copy the code

Re-examining the reBuild scope, you find that you have narrowed down to the TextWidget component, TextCom in the figure below

(2) RepaintBoundary separates the layer that needs to be updated in real time into a separate layer, which does not affect the drawing of other layers during redrawing