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) {
  const epsilon = .001;
  const sweep = _kTwoPi - epsilon;
  const startAngle = -math.pi / 2.0;

  final radius = size.shortestSide / 2.0;
  final center = Offset(size.width / 2.0, size.height / 2.0);
  final centerPoint = center;

  var pctTheta = (0.25 - (theta % _kTwoPi) / _kTwoPi) % 1.0;

  // Draw the background outer ring
  canvas.drawCircle(centerPoint, radius, Paint()..color = backgroundColor!);

  // Draw a translucent circle for every secondary unit
  for (var i = 0; i < baseUnitMultiplier; i = i + 1) {
    canvas.drawCircle(
        centerPoint, radius, Paint()..color = accentColor.withOpacity((i == 0) ? 0.3 : 0.1));
  }

  // Draw the inner background circle
  canvas.drawCircle(centerPoint, radius * 0.88, Paint()..color = Theme.of(context).canvasColor);

  // Get the offset point for an angle value of theta, and a distance of _radius
  Offset getOffsetForTheta(double theta, double radius) {
    return center + Offset(radius * math.cos(theta), -radius * math.sin(theta));
  }

  // Draw the handle that is used to drag and to indicate the position around the circle
  final handlePaint = Paint()..color = accentColor;
  final handlePoint = getOffsetForTheta(theta, radius - 10.0);
  canvas.drawCircle(handlePoint, 20.0, handlePaint);

  // Get the appropriate base unit string
  String getBaseUnitString() {
    switch (baseUnit) {
      case BaseUnit.millisecond:
        return 'ms.';
      case BaseUnit.second:
        return 'sec.';
      case BaseUnit.minute:
        return 'min.';
      case BaseUnit.hour:
        return 'hr.';
    }
  }

  // Get the appropriate secondary unit string
  String getSecondaryUnitString() {
    switch (baseUnit) {
      case BaseUnit.millisecond:
        return 's ';
      case BaseUnit.second:
        return 'm ';
      case BaseUnit.minute:
        return 'h ';
      case BaseUnit.hour:
        return 'd ';
    }
  }

  // Draw the Text in the center of the circle which displays the duration string
  var secondaryUnits =
      (baseUnitMultiplier == 0) ? '' : '$baseUnitMultiplier${getSecondaryUnitString()} ';
  var baseUnits = '$baseUnitHand';

  var textDurationValuePainter = TextPainter(
      textAlign: TextAlign.center,
      text: TextSpan(
          text: '$secondaryUnits$baseUnits',
          style: Theme.of(context)
              .textTheme
              .headline2!
              .copyWith(fontSize: size.shortestSide * 0.15)),
      textDirection: TextDirection.ltr)
    ..layout();
  var middleForValueText = Offset(centerPoint.dx - (textDurationValuePainter.width / 2),
      centerPoint.dy - textDurationValuePainter.height / 2);
  textDurationValuePainter.paint(canvas, middleForValueText);

  var textMinPainter = TextPainter(
      textAlign: TextAlign.center,
      text: TextSpan(
          text: getBaseUnitString(), //th: ${theta}',
          style: Theme.of(context).textTheme.bodyText2),
      textDirection: TextDirection.ltr)
    ..layout();
  textMinPainter.paint(
      canvas,
      Offset(centerPoint.dx - (textMinPainter.width / 2),
          centerPoint.dy + (textDurationValuePainter.height / 2) - textMinPainter.height / 2));

  // Draw an arc around the circle for the amount of the circle that has elapsed.
  var elapsedPainter = Paint()
    ..style = PaintingStyle.stroke
    ..strokeCap = StrokeCap.round
    ..color = accentColor.withOpacity(0.3)
    ..isAntiAlias = true
    ..strokeWidth = radius * 0.12;

  canvas.drawArc(
    Rect.fromCircle(
      center: centerPoint,
      radius: radius - radius * 0.12 / 2,
    ),
    startAngle,
    sweep * pctTheta,
    false,
    elapsedPainter,
  );

  // Paint the labels (the minute strings)
  void paintLabels(List<TextPainter> labels) {
    final labelThetaIncrement = -_kTwoPi / labels.length;
    var labelTheta = _kPiByTwo;

    for (var label in labels) {
      final labelOffset = Offset(-label.width / 2.0, -label.height / 2.0);

      label.paint(canvas, getOffsetForTheta(labelTheta, radius - 40.0) + labelOffset);

      labelTheta += labelThetaIncrement;
    }
  }

  paintLabels(labels);
}