paint.dart 6.31 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,
    );
    int colorRangeIndex = (sweepAngle / 360).ceil() - 1;
    if (colorRangeIndex > progressColor.length - 1) {
      colorRangeIndex = progressColor.length - 1;
    }
    if (colorRangeIndex < 0) {
      colorRangeIndex = 0;
    }

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

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

    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;
      }

      double actualCapAngle = capAngle;
      if (sweepAngle < 360) {
        if (sweepAngle < 2 * capAngle) {
          actualCapAngle = sweepAngle / 2;
        }
      } else {
        actualCapAngle = 0;
      }

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

      canvas.save();
      // Move the canvas origin to the center
      canvas.translate(centerX, centerY);
      double rotationAngle = 0;

      if (sweepAngle > 360) {
        rotationAngle = 360 - sweepAngle;
      }
      double rotationRadians =
          reverse ? -rotationAngle.toRadians() : rotationAngle.toRadians();
      // Rotate the canvas
      canvas.rotate((-90).toRadians() - rotationRadians);
      // Move the canvas origin back
      canvas.translate(-centerX, -centerY);

      if (sweepAngle < 360) {
        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 = progressColor[0].first,
        );
        canvas.drawRect(
          Rect.fromCenter(
            center: Offset(radius, 0),
            width: strokeWidth,
            height: 1,
          ),
          Paint()..color = progressColor[0].first,
        );
        canvas.restore();
      }

      canvas.drawArc(
        rect,
        reverse ? -startAngle.toRadians() : startAngle.toRadians(),
        reverse
            ? -drawnSweepAngle.toRadians()
            : drawnSweepAngle.toRadians(),
        false,
        paint,
      );

      if (sweepAngle != 360) {
        final endAngle = startAngle + drawnSweepAngle;
        canvas.save();
        canvas.translate(centerX, centerY);
        canvas.rotate(reverse ? -endAngle.toRadians() : endAngle.toRadians());

        final endColor =
            getColorAtAngle(sweepAngle, progressColor[colorRangeIndex]);
        
        final bool shouldDrawShadow = sweepAngle > 360 || (sweepAngle > 355 && sweepAngle < 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();
      }

      // // Restore the canvas to the saved state
      canvas.restore();
    }
  }

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

  /// get color by and angle
  Color getColorAtAngle(double angle, List<Color> colors) {
    double totalAngle = sweepAngle > 360 ? sweepAngle : 360;
    double fraction = angle / totalAngle;
    int segments = colors.length - 1;
    double segmentFraction = fraction * segments;
    int segmentIndex = segmentFraction.floor();
    double segmentInnerFraction = segmentFraction - segmentIndex;
    Color startColor = colors[segmentIndex];
    Color endColor = colors[(segmentIndex + 1) % colors.length];
    return Color.lerp(startColor, endColor, segmentInnerFraction)!;
  }
}