Search code examples
flutterflutter-dependenciesflutter-animation

Flutter - Custom timer with custom images for path & icon


So, I want to show a timer. Below is a simple design of what I want to achieve. The car should go around the track as the time gradually decreases. I currently have no idea how to implement it. There is also the thing where the face of the car should rotate as it goes around the loop. If someone has done something similar in the past, please do let me know. Any guides/tutorials would be greatly appreciated. enter image description here


Solution

  • Add package: path_drawing

    And try to run this code. For now, I used a timer to update the position of the car you can do it based on your requirement.

    The car image is added in asset folder. Car Image used in this example.

    Center image is also added in asset folder ("pluto-done.png") and need to convert it because it'll also draw using custom painter.

    import 'dart:async';
    import 'dart:math';
    import 'dart:ui' as ui;
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:path_drawing/path_drawing.dart' as pd;
    
    class CustomRoadWithCar extends StatefulWidget {
      const CustomRoadWithCar({super.key});
    
      @override
      State<CustomRoadWithCar> createState() => _CustomRoadWithCarState();
    }
    
    class _CustomRoadWithCarState extends State<CustomRoadWithCar> {
      Timer? _timer;
      int _elapsedSeconds = 0;
    
      // Variable to control the movement of the car and path fill
      double _carProgress = 0.0;
    
      ui.Image? carImage;
      ui.Image? centerImage;
    
      @override
      void initState() {
        super.initState();
        // Load the car image from the asset
        loadImageFromAsset('assets/car.png', isCarImage: true);
        // Load the center image from the asset
        loadImageFromAsset('assets/pluto-done.png', isCarImage: false);
        startTimer();
      }
    
      void startTimer() {
        _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
          setState(() {
            _elapsedSeconds++;
            // Calculate the progress based on elapsed seconds
            _carProgress = _elapsedSeconds / 59.0;
    
            // Check if the elapsed time has reached 59 seconds
            if (_elapsedSeconds >= 59) {
              // Stop the timer
              _timer?.cancel();
            }
          });
        });
      }
    
      @override
      void dispose() {
        // Cancel the timer when the widget is disposed
        _timer?.cancel();
        super.dispose();
      }
    
      // Load an image from assets and convert it to a ui.Image object
      Future<void> loadImageFromAsset(String path,
          {required bool isCarImage}) async {
        // Load the asset data
        final ByteData data = await rootBundle.load(path);
        final Uint8List bytes = data.buffer.asUint8List();
    
        // Decode the image data
        final ui.Codec codec = await ui.instantiateImageCodec(bytes);
        final ui.FrameInfo frameInfo = await codec.getNextFrame();
        final ui.Image loadedImage = frameInfo.image;
    
        // Set the loaded image to the appropriate state variable
        setState(() {
          if (isCarImage) {
            // If the image is for the car, set it to the image variable
            carImage = loadedImage;
          } else {
            // If the image is for the center of the circle, set it to the centerImage variable
            centerImage = loadedImage;
          }
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: const Text('Circle Border Drawing'),
          ),
          body: Center(
            child: Stack(
              children: [
                //grey color road
                CustomPaint(
                  size: const Size(
                      200, 200), // Set the size of the CustomPaint widget
                  painter: RoadPainter(25.0), // Set the border width
                ),
                //to be filled by blue color
                CustomPaint(
                  size: const Size(
                      200, 200), // Set the size of the CustomPaint widget
                  painter: CircleBorderPainter(
                      carProgress:
                          _carProgress, // Pass the current progress to the painter
                      borderWidth: 25.0,
                      image: carImage,
                      centerImage: centerImage,
                      fillColor: const Color(0xff243347)),
                ),
                Text("$_elapsedSeconds")
              ],
            ),
          ),
        );
      }
    }
    
    class CircleBorderPainter extends CustomPainter {
      final double carProgress; // Progress of the car and path fill (0.0 to 1.0)
      final double borderWidth;
      final ui.Image? image;
      final ui.Image? centerImage;
      final Color fillColor;
    
      CircleBorderPainter(
          {required this.carProgress,
          required this.borderWidth,
          required this.image,
          required this.fillColor,
          required this.centerImage});
    
      @override
      void paint(Canvas canvas, Size size) {
        final double radius = size.width / 2;
        final Paint paint = Paint()
          ..color = fillColor
          ..style = PaintingStyle.stroke
          ..strokeWidth = borderWidth
          ..strokeCap = StrokeCap.round;
    
        // Calculate the arc angle based on progress
        double sweepAngle = carProgress * 2 * pi;
    
        final center = size.center(Offset.zero);
    
        // Draw the arc up to the current progress
        Rect rect = Rect.fromCircle(center: center, radius: radius);
        canvas.drawArc(rect, -pi / 2, sweepAngle, false, paint);
    
        // Calculate the car's position along the arc based on progress
        double carAngle = -pi / 2 + sweepAngle;
        double carX = size.width / 2 + radius * cos(carAngle);
        double carY = size.height / 2 + radius * sin(carAngle);
        Offset carPosition = Offset(carX, carY);
    
        DashedCirclePainter dashedCirclePainter = DashedCirclePainter(
          strokeWidth: 1.0,
          color: Colors.white,
          dashPattern: [10.0, 5.0], // Dash length and gap length
        );
        dashedCirclePainter.paint(canvas, size, center, radius);
    
        // Draw the car image at the calculated position
        if (image != null) {
          // Desired image width set to 24
          double desiredImageWidth = 24;
    
          // Calculate the image aspect ratio
          double imageAspectRatio =
              image!.width.toDouble() / image!.height.toDouble();
    
          // Calculate the height based on the desired width and aspect ratio
          double desiredImageHeight = desiredImageWidth / imageAspectRatio;
    
          // Save the canvas state
          canvas.save();
    
          // Translate the canvas to the car position
          canvas.translate(carPosition.dx, carPosition.dy);
    
          // Rotate the canvas based on the car's angle
          canvas.rotate(carAngle);
    
          // Draw the car image at the car position
          canvas.drawImageRect(
            image!,
            Rect.fromLTWH(0, 0, image!.width.toDouble(), image!.height.toDouble()),
            Rect.fromCenter(
                center: Offset.zero,
                width: desiredImageWidth,
                height: desiredImageHeight),
            Paint(),
          );
    
          // Restore the canvas state
          canvas.restore();
        }
    
    // Draw the image at the center of the circle
        if (centerImage != null) {
          // Desired image width set to 50
          double desiredImageWidth = 50;
    
          // Calculate the image aspect ratio
          double centerImageAspectRatio =
              centerImage!.width.toDouble() / centerImage!.height.toDouble();
    
          // Calculate the height based on the desired width and aspect ratio
          double desiredImageHeight = desiredImageWidth / centerImageAspectRatio;
    
          // Calculate the rectangle where the image should be drawn
          Rect imageRect = Rect.fromCenter(
            center: center,
            width: desiredImageWidth,
            height: desiredImageHeight,
          );
    
          // Draw the image at the center of the circle
          canvas.drawImageRect(
            centerImage!,
            Rect.fromLTWH(0, 0, centerImage!.width.toDouble(),
                centerImage!.height.toDouble()),
            imageRect,
            Paint(),
          );
        }
      }
    
      @override
      bool shouldRepaint(CircleBorderPainter oldDelegate) {
        return oldDelegate.carProgress != carProgress ||
            oldDelegate.borderWidth != borderWidth ||
            oldDelegate.image != image;
      }
    }
    
    class RoadPainter extends CustomPainter {
      final double borderWidth;
    
      RoadPainter(this.borderWidth);
    
      @override
      void paint(Canvas canvas, Size size) {
        // Paint for the base road
        final Paint roadPaint = Paint()
          ..color = Colors.grey // Grey color for the road
          ..style = PaintingStyle.stroke
          ..strokeWidth = borderWidth;
    
        // Calculate the radius and center of the canvas
        final double radius = size.width / 2;
        final Offset center = size.center(Offset.zero);
    
        // Draw the base circle (road) with specified border width
        canvas.drawCircle(center, radius, roadPaint);
    
        DashedCirclePainter dashedCirclePainter = DashedCirclePainter(
          strokeWidth: 1.0,
          color: Colors.white,
          dashPattern: [10.0, 5.0], // Dash length and gap length
        );
        dashedCirclePainter.paint(canvas, size, center, radius);
      }
    
      @override
      bool shouldRepaint(RoadPainter oldDelegate) {
        return oldDelegate.borderWidth != borderWidth;
      }
    }
    
    class DashedCirclePainter {
      final double strokeWidth;
      final Color color;
      final List<double> dashPattern;
    
      DashedCirclePainter({
        required this.strokeWidth,
        required this.color,
        required this.dashPattern,
      });
    
      void paint(Canvas canvas, Size size, Offset center, double radius) {
        // Paint for the red dashed circle
        final Paint dashedCirclePaint = Paint()
          ..color = color // Color for the dashed circle
          ..style = PaintingStyle.stroke
          ..strokeWidth = strokeWidth;
    
        // Create a Path for the red dashed circle
        Path dashedCirclePath = Path();
        dashedCirclePath.addOval(Rect.fromCircle(center: center, radius: radius));
    
        // Draw the dashed red circle using dashPath function
        canvas.drawPath(
          pd.dashPath(
            dashedCirclePath,
            dashArray: pd.CircularIntervalList<double>(dashPattern),
          ),
          dashedCirclePaint,
        );
      }
    }
    
    

    Hope It helps...