paint method

  1. @override
void paint(
  1. Canvas canvas,
  2. Size size
)
override

Called whenever the object needs to paint. The given Canvas has its coordinate space configured such that the origin is at the top left of the box. The area of the box is the size of the size argument.

Paint operations should remain inside the given area. Graphical operations outside the bounds may be silently ignored, clipped, or not clipped. It may sometimes be difficult to guarantee that a certain operation is inside the bounds (e.g., drawing a rectangle whose size is determined by user inputs). In that case, consider calling Canvas.clipRect at the beginning of paint so everything that follows will be guaranteed to only draw within the clipped area.

Implementations should be wary of correctly pairing any calls to Canvas.save/Canvas.saveLayer and Canvas.restore, otherwise all subsequent painting on this canvas may be affected, with potentially hilarious but confusing results.

To paint text on a Canvas, use a TextPainter.

To paint an image on a Canvas:

  1. Obtain an ImageStream, for example by calling ImageProvider.resolve on an AssetImage or NetworkImage object.

  2. Whenever the ImageStream's underlying ImageInfo object changes (see ImageStream.addListener), create a new instance of your custom paint delegate, giving it the new ImageInfo object.

  3. In your delegate's paint method, call the Canvas.drawImage, Canvas.drawImageRect, or Canvas.drawImageNine methods to paint the ImageInfo.image object, applying the ImageInfo.scale value to obtain the correct rendering size.

Implementation

@override
void paint(Canvas canvas, Size size) {
  // Whether the shape that we're outlining is so thin that it is effectively
  // a line rather than a quadrilateral.
  final bool isLine =
      size.shortestSide <= strokeWidth * 2 && boxShape == BoxShape.rectangle;
  final bool isHorizLine = isLine && size.width <= strokeWidth * 2;
  final bool isVertLine = isLine && size.height <= strokeWidth * 2;

  final Paint paint = Paint()
    ..color = color
    ..style = PaintingStyle.stroke
    ..strokeWidth = strokeWidth
    ..strokeCap = strokeCap.flutterStrokeCap
    ..strokeMiterLimit = strokeMiterLimit;

  // If the stroke is dotted, we take a completely different rendering
  // route.
  if (dashPattern.isNotEmpty) {
    final Path path = Path();

    // Let's first create the appropriate path. If it's a circle,
    // It doesn't matter if the it's an outside/inside/center stroke yet.
    // We will be doing manual calculations to figure that out below.
    // For now, we just want the border path.
    if (isLine) {
      path
        ..moveTo(
          isHorizLine ? strokeWidth / 2 : 0,
          isVertLine ? strokeWidth / 2 : 0,
        )
        ..lineTo(
          isHorizLine ? strokeWidth / 2 : size.width,
          isVertLine ? strokeWidth / 2 : size.height,
        );
      paint.style = PaintingStyle.stroke;
    } else {
      if (boxShape == BoxShape.circle) {
        final double circleDiameter = min(size.width, size.height);
        final Rect basicRect = Rect.fromCenter(
            center: Offset(size.width / 2, size.height / 2),
            width: circleDiameter,
            height: circleDiameter);
        path.addOval(basicRect);
      } else {
        final Rect basicRect = Rect.fromLTWH(0, 0, size.width, size.height);
        final RRect roundedBox = borderRadius.toRRect(basicRect);
        path.addRRect(roundedBox);
      }
    }

    // We loop over each "segment" of the path. Not up to us to know what
    // those segments are, we just work with them. Flutter likes being
    // mysterious.
    for (final PathMetric metric in path.computeMetrics()) {
      double totalDistance = 0;
      bool draw = true;

      // We need to draw a dashed line, so we iterate with a while loop
      // our desired dash distance.
      while (totalDistance < metric.length) {
        final double dashDistance = dashArray.nextValue();

        if (draw) {
          // Now that we're about to draw a singular dash, we need to do it
          // manually. We do this with tangent calculations. Luckily for us,
          // Flutter has a convenient method for that!
          final Tangent? tangent = metric.getTangentForOffset(totalDistance);

          if (tangent != null) {
            // The tangent is always the PARALLEL vector for a given
            // line. So imagine a ball on a table. The table's line is a
            // tangent line on the ball's specific intersection point with
            // the line.
            //
            // We don't want that though, we want an actual perpendicular line
            // like a pin on a circle. To do that, we just calculate the
            // perpendicular vector of that. It's easy in 2D, it's just
            // (-y, x) or (y, -x)
            //
            // And here, since this is a pin coming out of a circle, we want
            // the pin to actually be either outside the circle or inside
            // the circle; inside stroke or outside stroke. This is where
            // isInside, isCenter, and isOutside come in. They are changing
            // the vector's direction between (-y, x) or (y, -x), flipping
            // the direction for our convenience.
            final Offset parallelTangentVector = tangent.vector;
            final Offset normal = Offset(
                  parallelTangentVector.dy * isInside * isCenter,
                  parallelTangentVector.dx * isOutside,
                ) *
                // Multiply by the stroke thickness to figure out the distance
                // needed from the start position to the stroke.
                strokeWidth *

                // If it's a center stroke, we need to divide the normal by 2.
                // It will be rendered twice, once inside, once outside,
                // achieving the center stroke effect.
                (strokeAlign == StrokeAlignC.center ? 0.5 : 1);

            canvas.drawLine(
              tangent.position,
              tangent.position + normal,

              // The width of the "needle" on the "balloon"
              paint..strokeWidth = dashDistance,
            );

            // If it's a center stroke though, we draw another line in the
            // opposite direction alongside the earlier line.
            if (strokeAlign == StrokeAlignC.center) {
              canvas.drawLine(
                tangent.position,

                // Rendering the needle as if it were on the inside of the
                // balloon.
                tangent.position + normal * -1,

                // The width of the "needle" on the "balloon"
                paint..strokeWidth = dashDistance,
              );
            }
          }
        }
        totalDistance += dashDistance;
        draw = !draw;
      }
    }
  } else {
    // These variables create a new rectangle that is the "real" "visually
    // accurate" rectangle. IE: a rectangle containing the stroke inside its
    // borders.
    Offset topLeft = Offset.zero;
    Size realSize = size;
    switch (strokeAlign) {
      case StrokeAlignC.center:
        topLeft = Offset(-strokeWidth / 2, -strokeWidth / 2);
        realSize = Size(size.width + strokeWidth, size.height + strokeWidth);
      case StrokeAlignC.outside:
        topLeft = Offset(-strokeWidth, -strokeWidth);
        realSize =
            Size(size.width + strokeWidth * 2, size.height + strokeWidth * 2);
      case StrokeAlignC.inside:
        break;
    }

    // Outside strokes get tricky (yes they really do, trust me).
    // We have to handle them in a completely different way.
    if (strokeAlign == StrokeAlignC.outside) {
      // We create 2 paths. One is the normal border, the other is the
      // outer edge of the stroke. We will fill in the space between them
      // with a color to get our desired result.
      final Path innerPath = Path();
      final Path outerPath = Path();

      if (boxShape == BoxShape.circle) {
        final double circleDiameter = min(size.width, size.height);
        final Rect basicRect = Rect.fromCenter(
            center: Offset(size.width / 2, size.height / 2),
            width: circleDiameter,
            height: circleDiameter);
        innerPath.addOval(basicRect);
        outerPath.addOval(basicRect.inflate(strokeWidth));
      } else {
        final Rect basicRect = Rect.fromLTWH(0, 0, size.width, size.height);

        final RRect roundedBox = borderRadius.toRRect(basicRect);
        innerPath.addRRect(roundedBox);

        // The outer path is a bit tricky. You can't just inflate the rect
        // because if it has no borders, it will add a border anyways, which
        // really isn't nice. So to get around that, we interpolate a magic
        // number to EASE the (manual) inflation instead.
        //
        // In the end of the
        // day, you always need to make the borders larger for the outer edge
        // of the outside strike for it to look "normal".
        //
        // We just don't want
        // that inflation at no borders and we don't want it to instantly
        // appear after a border of 1 pixel exists (it'll effectively double).
        // So we just interpolate the first 10 pixels to ease the inflated
        // border in as the border radius increases on each corner.
        //
        // We get a really nice and seamless outside border transition this
        // way.
        outerPath.addRRect((borderRadius.add(
          BorderRadius.only(
            topRight: Radius.circular(
                strokeWidth * (min(borderRadius.topRight.x, 10) / 10)),
            topLeft: Radius.circular(
                strokeWidth * (min(borderRadius.topLeft.x, 10) / 10)),
            bottomLeft: Radius.circular(
                strokeWidth * (min(borderRadius.bottomLeft.x, 10) / 10)),
            bottomRight: Radius.circular(
                strokeWidth * (min(borderRadius.bottomRight.x, 10) / 10)),
          ),
        ) as BorderRadius)
            .toRRect(
          Rect.fromLTWH(
            topLeft.dx,
            topLeft.dy,
            realSize.width,
            realSize.height,
          ),
        ));
      }

      // This is where the magic happens. We want to fill the area
      // between two paths. To do that, there's this magic feature in
      // Flutter paths called FillType.evenOdd. It does the work for us.
      // It's effectively a mask system.
      final Path dest = Path()..fillType = PathFillType.evenOdd;
      dest.addPath(innerPath, Offset.zero);
      dest.addPath(outerPath, Offset.zero);

      canvas.drawPath(
        dest..close(),
        paint..style = PaintingStyle.fill,
      );
    } else {
      final Rect rect = Rect.fromLTWH(
          topLeft.dx, topLeft.dy, realSize.width, realSize.height);

      // For any other border, we can just use Flutter's built in painter.
      // No special handling required here.
      border.paint(
        canvas,
        rect,
        borderRadius: boxShape == BoxShape.circle ? null : borderRadius,
        shape: boxShape,
      );
    }
  }
}