rendering


Dismantlement of effect drawing

  1. The diagram contains two parts, the upper part is the candle diagram, the lower part is the volume
  2. The two figures are divided into two parts, the background grid and the content part, and there is a certain correlation
  3. The X axis of the volume of the candle chart is the time line, and the time in the same position is the same, that is, the time in the vertical direction is synchronous.
  4. The Y-axis represents the range of numbers.
  5. The candle diagram has a lot in common with volume, so the code structure can be reused.

Extraction drawing Widget

class XRenderWidget<T extends ChangeNotifier> extends LeafRenderObjectWidget {
  BaseRender baseRender;
  XRenderWidget({Key key, this.baseRender}) : super(key: key);
  @override
  RenderObject createRenderObject(Object context) {
    try {
      Provider.of<T>(context);
    } catch (Exception) {
      // ignore
    }
    return XRenderBox(baseRender: baseRender);
  }

  @override
  void updateRenderObject(BuildContext context, XRenderBox renderObject) {
    super.updateRenderObject(context, renderObject);
    print("$baseRender updateRenderObject"); renderObject.updateRender(); }}Copy the code

XRenderWidget does two things

  1. Create a RenderObject
  2. Update the RenderObject

After rebuild, updateRenderObject is executed. Similar to StatefulWidget, RenderObject is reused to render the UI for increased utilization

Extract the drawing RenderBox

class XRenderBox extends RenderBox {
  BaseRender baseRender;

  XRenderBox({this.baseRender});

  @override
  void performLayout() { super.performLayout(); baseRender? .onPerformLayout(size); } @override void paint(PaintingContext context, Offset offset) { super.paint(context, offset); baseRender? .onPaint(context, offset); } @override bool get sizedByParent =>true;

  @override
  bool hitTestSelf(Offset position) => true;

  void updateRender() {
    baseRender?.updateRender();
    markNeedsPaint();
  }
}
Copy the code

XRenderBox is simple, with some core functions statically proxied

What did BaseRender do?

So let’s think about what the initial effect is unraveling.

Yeah, that’s probably what I did

  1. Store the Size of the RenderObject
  Size size;
Copy the code
  1. There is also a grid at the bottom of the candle in the renderings, so UI level processing is also required
  List<T> _aboveChildren = [];
  List<T> _underChildren = [];
  BaseMaxMinRender parent;

  void addChild(T child, {int elevation = 0}) {
    if (elevation < 0) {
      _underChildren.add(child);
    } else {
      _aboveChildren.add(child);
    }
  }

  void delChild(T child) {
    _aboveChildren.remove(child);
    _underChildren.remove(child);
  }

Copy the code
  1. The upper and lower boundaries of the Widget are the maximum and minimum values respectively, while the origin of the coordinates in the flutter is the upper left corner, the right direction is the positive and negative direction of X, and the downward direction is the positive and negative direction of Y. This is different from drawing the coordinate system, so the ability of coordinate system transformation is required.
Matrix4 _matrix4 = vm.matrix4. identity(); Matrix4 get matrix4 => parent? .matrix4 ?? _matrix4; void_calcMaxMin() {
    
    MaxMin newMaxMin; /// The maximum and minimum value of this layer is MaxMin cur = calcOwnMaxMin(a); // The maximum and minimum values of children are MaxMin children = _childrenMaxMin(a); /// No children, or children do not need to calculate the maximum and minimum is nullif (children == null) {
      newMaxMin = cur;
    } else{// merge your Max and min values with children to create a newMax and min value newMaxMin = cur.merge(children);
    }
    if (newMaxMin! = maxMin) {
      if (maxMin == null || maxMin.isZero()) {
        /// maxMinFor initialization, maxM is assigned directlyin = newMaxMin;
      } else{/// If the maximum and minimum values change, the experience will flow a lotin(newMaxMin);
      }
    }
  }

  /// 通过MaxMinThe transformation coefficients are stored in matrix4 to facilitate changes in the data void_transformMatrix() {
    if (_maxMin! = null) { _matrix4.setIdentity(); Var scaleY = (height-edgeinsets. Bottom-edgeinsets. Top) / _maxmin.delta; /// Set the offset of the matrix in the X-axis, since the minimum value in the graph does not always start at 0, Vector3(0, height-edgeinsets. Bottom + _maxmin.min * scaleY, 0.0)); // set the diagonal values of the matrix. The diagonal values are the scaling values of x, y, and z respectively. 1 means no scaling, -scaley means everything in the Y-axis is multiplied by -scaley, so you scale scaleY and reverse the Y-axis. _matrix4.setDiagonal(vm.Vector4(1, -scaleY, 1, 1)); }else{ _matrix4.setIdentity(); }}Copy the code

Candle drawing

Render with hierarchical processing, coordinate transformation ability, it can be convenient to draw the image.

class CandleRender extends BaseKLineRender { Paint _klinePaint = Paint(); List<double> wickData = []; List<double> candleData = []; CandleRender(ControllerModel controller) : super(controller); Color _itemColor(int i) => controller.getColorRelativeStartIndex(i); @override voidfillOwnData() { super.fillOwnData(); wickData.clear(); candleData.clear(); /// Iterate over the data that needs to be displayed on the screenforEachData((i) { double x = controller.getXByIndex(i); / / / three data represents a point (x, y, z), z is 0 here / / / add a candle core segment data wickData.. add(x).. add(klineData[i].high).. add(0).. add(x).. add(klineData[i].low).. add(0); // Add candleData.. add(x).. add(klineData[i].open).. add(0).. add(x).. add(klineData[i].close).. add(0); }); } @override voidtransformData() {/// Convert the data added above to the data on the screen through coordinate transformation. / / / the source data is added above (x, y, z), where x is the pixels on the screen, but is the price, y y matrix4. Also should make a certain scale and translation applyToVector3Array (wickData); matrix4.applyToVector3Array(candleData); } /// calculate the maximum and minimum values of this layer of itself. @override MaxMin calcOwnMaxMin() {
    double max = -double.maxFinite;
    double min = double.maxFinite;
    for (int i = controller.startIndex; i <= controller.endIndex; i++) {
      if (i == controller.startIndex) {
        min = klineData[i].low;
        max = klineData[i].high;
      } else{ max = math.max(max, klineData[i].high); min = math.min(min, klineData[i].low); }}return MaxMin(min: min, max: max); } @override void onRealPaint(Canvas canvas) { super.onRealPaint(canvas); _klinePaint.strokeWidth = 1; DrawLines (canvas, wickData, controller.needdrawcount (), _klinePaint, color: _itemColor); drawLines(canvas, wickData, controller.needdrawcount (), _klinePaint, color: _itemColor); _klinePaint.strokeWidth = controller.candleWidth - 1; /// The brush size of the candle body is -1, the width occupied by the candle, so that the candle has a gap of 1. DrawLines (canvas, candleData, Controller.needdrawcount (), _klinePaint, color: _itemColor); }}Copy the code

Plot volume

If you have a candle drawing, a volume drawing, it’s just the same filling data, calculating Max and min, converting data, drawing data.

class VolumeRender extends BaseKLineRender {
  Paint _klinePaint = Paint();
  List<double> _volData = [];

  VolumeRender(ControllerModel controller) : super(controller);

  Color _itemColor(int i) => controller.getColorRelativeStartIndex(i);

  @override
  void fillOwnData() {
    super.fillOwnData();
    _volData.clear();
    forEachData((i) { double x = controller.getXByIndex(i); _volData.. add(x).. add(klineData[i].amount).. add(0).. add(x).. add(0).. add(0); }); } @override voidtransformData() {
    matrix4.applyToVector3Array(_volData);
  }

  @override
  MaxMin calcOwnMaxMin() {
    double min = 0;
    double max = 0;
    forEachData((i) {
      max = math.max(max, klineData[i].amount);
    });
    return MaxMin(min: min, max: max); } @override void onRealPaint(Canvas canvas) { super.onRealPaint(canvas); _klinePaint.strokeWidth = controller.candleWidth - 1; drawLines(canvas, _volData, controller.needDrawCount(), _klinePaint, color: _itemColor); }}Copy the code

Draw a grid

Both candles and volumes have grids and are on the bottom layer

class GridLineRender extends _BaseGridLineRender { Paint _paint = Paint(); GridLineConfig gridLineConfig; GridLineRender(this.gridLineConfig, controller) : super(controller); @override void onRealPaint(Canvas canvas) { super.onRealPaint(canvas); /// coordinate transformation of the inverse, the purpose is to calculate the corresponding price of the screen coordinates. /// The price can be easily found online. Matrix4 m = matrix4.clone().. invert(); /// Draw a horizontal line according to the configurationfor (int i = 0; i < gridLineConfig.horizontalCount; i++) {
      double y = height / (gridLineConfig.horizontalCount - 1) * i;
      _paint.strokeWidth = gridLineConfig.horizontalStrokeWidth;
      _paint.color = gridLineConfig.horizontalColor;
      canvas.drawLine(Offset(0, y), Offset(width, y), _paint);
      if (isNotEmpty(klineData) && totalMaxMin! = null) { List<double> yy = [0, y, 0]; m.applyToVector3Array(yy);if (i == 0) {
          drawText(canvas, "${format ! = null ? format(yy[1]) : yy[1]}", Offset(width, y), align: TextAlign.end);
        } else if (i == gridLineConfig.horizontalCount - 1) {
          drawText(canvas, "${format ! = null ? format(yy[1]) : yy[1]}", Offset(width, y - 12), align: TextAlign.end);
        } else {
          drawText(canvas, "${format ! = null ? format(yy[1]) : yy[1]}", Offset(width, y - 12), align: TextAlign.end); }}} // draw vertical lines according to the configurationfor (int i = 0; i < gridLineConfig.verticalCount; i++) {
      double x = width / (gridLineConfig.verticalCount - 1) * i;
      _paint.strokeWidth = gridLineConfig.verticalStrokeWidth;
      _paint.color = gridLineConfig.verticalColor;
      canvas.drawLine(Offset(x, 0), Offset(x, height), _paint);
    }
  }
}

class GridLineConfig {
  int verticalCount = 6;
  Color verticalColor = Colors.grey[300];
  double verticalStrokeWidth = 0.5;

  int horizontalCount = 3;
  Color horizontalColor = Colors.grey[300];
  double horizontalStrokeWidth = 0.5;
}

Copy the code

Render added to widgets

// render volumeRender = volumeRender (_controllerModel); /// render candleRender = candleRender (_controllerModel); Candlerender.addchild (GridLineRender(GridLineConfig()).. horizontalCount = 5, _controllerModel) .. format = (double val) => formatNumber(val, 2), elevation: -1); Volumerender.addchild (GridLineRender(GridLineConfig(), _controllerModel).. format = (double val) => formatNumber(val, 2), elevation: -1);return MultiProvider(
        providers: [
          ChangeNotifierProvider<DataModel>(create: (_) => _dataModel),
          ChangeNotifierProvider<ConfigModel>(create: (_) => _configModel),
          ChangeNotifierProvider<ControllerModel>(create: (_) => _controllerModel),
          ChangeNotifierProvider<KLineHighlightModel>(create: (_) => _hightLightModel),
        ],
        child: _wrapperGesture(
          Consumer<ControllerModel>(builder: (context, controllerModel, child) {
            _logger.debug("Consumer KLineControllerModel");
            returnColumn( children: <Widget>[ xRenderWidget<DataModel>(candleRender, height: 200), xRenderWidget<DataModel>(volumeRender, height: 100), ], ); })));Copy the code

conclusion

Now we have finished the drawing of the candle chart and the drawing of the volume, and we have a preliminary look. Some of the code in the article is ugly, such as written in the reorganization.

The next section will talk about gesture processing