paint.dart 7.4 KB
import 'dart:math' show min, pi, asin;

import 'package:flutter/material.dart';

/// convert
extension NumToRadians on num {
  /// toRadians
  double toRadians() {
    return toDouble() * (pi / 180.0);
  }

  /// toDegree
  double toDegree() {
    return toDouble() * (180 / pi);
  }
}

/// painter for progress
class ProgressPainter extends CustomPainter {
  ///0 to max
  final double sweepAngle;

  /// circle stroke width
  final double strokeWidth;

  /// multi color
  final List<List<Color>> progressColor;

  /// base  background color
  final Color backgroundColor;

  /// reverse
  final bool reverse;

  /// painter for progress
  ProgressPainter({
    this.sweepAngle = 360,
    this.strokeWidth = 20,
    this.reverse = false,
    required this.progressColor,
    required this.backgroundColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    double wh = min(size.width, size.height);
    var centerX = wh / 2;
    var centerY = wh / 2;
    var radius = wh / 2 - strokeWidth / 2;

    var rect = Rect.fromCenter(
      center: Offset(centerX, centerY),
      width: wh - strokeWidth,
      height: wh - strokeWidth,
    );

    var backgroundPaint = Paint()
      ..strokeWidth = strokeWidth
      ..color = backgroundColor
      ..strokeCap = StrokeCap.butt
      ..style = PaintingStyle.stroke;

    canvas.drawArc(
      rect,
      0.toRadians(),
      360.toRadians(),
      false,
      backgroundPaint,
    );

    if (sweepAngle > 0) {
      double capAngle = 0;
      if (radius > 0) {
        capAngle = asin((strokeWidth / 2) / radius) * 180 / pi;
      }

      int fullLaps = (sweepAngle / 360).floor();
      double remainingAngle = sweepAngle % 360;

      // Draw full laps first
      for (int i = 0; i < fullLaps; i++) {
        int colorIdx = i;
        if (colorIdx > progressColor.length - 1) {
          colorIdx = progressColor.length - 1;
        }
        var shader = SweepGradient(
          startAngle: 0.toRadians(),
          endAngle: 360.toRadians(),
          colors: reverse
              ? progressColor[colorIdx].reversed.toList()
              : progressColor[colorIdx],
        ).createShader(rect);

        var lapPaint = Paint()
          ..strokeWidth = strokeWidth
          ..shader = shader
          ..strokeCap = StrokeCap.butt
          ..style = PaintingStyle.stroke;

        canvas.save();
        canvas.translate(centerX, centerY);
        canvas.rotate((-90).toRadians());
        canvas.translate(-centerX, -centerY);

        canvas.drawArc(
          rect,
          0.toRadians(),
          reverse ? -360.toRadians() : 360.toRadians(),
          false,
          lapPaint,
        );

        canvas.restore();
      }

      // Draw the remaining fractional lap
      if (remainingAngle > 0.0) {
        int colorIdx = fullLaps;
        if (colorIdx > progressColor.length - 1) {
          colorIdx = progressColor.length - 1;
        }

        var shader = SweepGradient(
          startAngle: 0.toRadians(),
          endAngle: 360.toRadians(),
          colors: reverse
              ? progressColor[colorIdx].reversed.toList()
              : progressColor[colorIdx],
        ).createShader(rect);

        var lapPaint = Paint()
          ..strokeWidth = strokeWidth
          ..shader = shader
          ..strokeCap = StrokeCap.butt
          ..style = PaintingStyle.stroke;

        double actualCapAngle = capAngle;
        if (remainingAngle < 2 * capAngle) {
          actualCapAngle = remainingAngle / 2;
        }

        double startAngle = actualCapAngle;
        double drawnSweepAngle = remainingAngle - 2 * actualCapAngle;
        if (drawnSweepAngle < 0) {
          drawnSweepAngle = 0;
        }

        canvas.save();
        canvas.translate(centerX, centerY);
        canvas.rotate((-90).toRadians());
        canvas.translate(-centerX, -centerY);

        // Draw start cap for this fractional lap
        canvas.save();
        canvas.translate(centerX, centerY);
        canvas
            .rotate(reverse ? -startAngle.toRadians() : startAngle.toRadians());
        canvas.drawArc(
          Rect.fromCenter(
            center: Offset(radius, 0),
            width: strokeWidth,
            height: strokeWidth,
          ),
          0,
          reverse ? 180.toRadians() : -180.toRadians(),
          true,
          Paint()
            ..color = reverse
                ? progressColor[colorIdx].last
                : progressColor[colorIdx].first,
        );
        canvas.drawRect(
          Rect.fromCenter(
            center: Offset(radius, 0),
            width: strokeWidth,
            height: 1,
          ),
          Paint()
            ..color = reverse
                ? progressColor[colorIdx].last
                : progressColor[colorIdx].first,
        );
        canvas.restore();

        // Draw the fractional arc
        canvas.drawArc(
          rect,
          reverse ? -startAngle.toRadians() : startAngle.toRadians(),
          reverse ? -drawnSweepAngle.toRadians() : drawnSweepAngle.toRadians(),
          false,
          lapPaint,
        );

        // Draw end cap for this fractional lap
        final endAngle = startAngle + drawnSweepAngle;
        canvas.save();
        canvas.translate(centerX, centerY);
        canvas.rotate(reverse ? -endAngle.toRadians() : endAngle.toRadians());

        final endColor = getColorAtAngleOf360(
          remainingAngle,
          reverse
              ? progressColor[colorIdx].reversed.toList()
              : progressColor[colorIdx],
        );

        final bool shouldDrawShadow =
            sweepAngle > 360 || (remainingAngle > 355 && remainingAngle < 360);
        if (shouldDrawShadow) {
          //Draw arc shadow
          var shadowPosition = Offset(
            radius,
            reverse ? -4 : 4,
          );
          final Paint shadowPaint = Paint()
            ..color = Colors.black.withOpacity(0.35)
            ..style = PaintingStyle.fill
            ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3);
          canvas.drawCircle(shadowPosition, strokeWidth / 2, shadowPaint);
        }

        //Draw the ending arc
        canvas.drawArc(
          Rect.fromCenter(
            center: Offset(radius, 0),
            width: strokeWidth,
            height: strokeWidth,
          ),
          0,
          reverse ? -180.toRadians() : 180.toRadians(),
          true,
          Paint()..color = endColor,
        );
        canvas.drawRect(
          Rect.fromCenter(
            center: Offset(radius, 0),
            width: strokeWidth,
            height: 1,
          ),
          Paint()..color = endColor,
        );
        canvas.restore();

        canvas.restore();
      }
    }
  }

  @override
  bool shouldRepaint(covariant ProgressPainter oldDelegate) {
    return sweepAngle != oldDelegate.sweepAngle;
  }

  /// get color by angle of a single 360 degree span
  Color getColorAtAngleOf360(double angle, List<Color> colors) {
    double fraction = angle / 360.0;
    if (fraction > 1.0) fraction = 1.0;
    if (fraction < 0.0) fraction = 0.0;
    int segments = colors.length - 1;
    if (segments <= 0) return colors.first;
    double segmentFraction = fraction * segments;
    int segmentIndex = segmentFraction.floor();
    if (segmentIndex >= segments) segmentIndex = segments - 1;
    double segmentInnerFraction = segmentFraction - segmentIndex;
    Color startColor = colors[segmentIndex];
    Color endColor = colors[segmentIndex + 1];
    return Color.lerp(startColor, endColor, segmentInnerFraction)!;
  }
}