skip to Main Content

My code as of now:

class RoundedRectanglePainter extends CustomPainter {
  final Color strokeColorGradientStart;
  final Color strokeColorGradientEnd;
  final double strokeWidth;
  final double borderRadius;
  final Color fillColorGradientStart;
  final Color fillColorGradientEnd;
  final Animation<double> animation;

  RoundedRectanglePainter({
    required this.strokeColorGradientStart,
    required this.strokeColorGradientEnd,
    required this.strokeWidth,
    required this.borderRadius,
    required this.fillColorGradientStart,
    required this.fillColorGradientEnd,
    required this.animation,
  }) : super(repaint: animation);

  @override
  void paint(Canvas canvas, Size size) {
    final strokeGradient = LinearGradient(
      colors: [strokeColorGradientStart, strokeColorGradientEnd],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    );

    final fillGradient = LinearGradient(
      colors: [fillColorGradientStart, fillColorGradientEnd],
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    );

    final strokePaint = Paint()
      ..shader = strokeGradient
          .createShader(Rect.fromLTWH(0, 0, size.width, size.height))
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke;

    final fillPaint = Paint()
      ..shader = fillGradient
          .createShader(Rect.fromLTWH(0, 0, size.width, size.height))
      ..style = PaintingStyle.fill;

    final outerRect = Rect.fromLTWH(0, 0, size.width, size.height);
    final outerRRect =
        RRect.fromRectAndRadius(outerRect, Radius.circular(borderRadius));
    canvas.drawRRect(outerRRect, strokePaint);

    final innerWidth = size.width - (strokeWidth * 2) - 10;
    final innerHeight = size.height - (strokeWidth * 2) - 10;
    final innerRect = Rect.fromLTWH((size.width - innerWidth) / 2,
        (size.height - innerHeight) / 2, innerWidth, innerHeight);
    final innerRRect =
        RRect.fromRectAndRadius(innerRect, Radius.circular(borderRadius));

    canvas.drawRRect(innerRRect, fillPaint);

    Path outerPath = Path()..addRRect(outerRRect);
    PathMetrics pathMetrics = outerPath.computeMetrics();
    PathMetric pathMetric = pathMetrics.first;

    double currentLength = pathMetric.length * animation.value;
    Tangent? tangent = pathMetric.getTangentForOffset(currentLength);

    if (tangent != null) {
      final movingLinePaint = Paint()
        ..color = Colors.red
        ..strokeWidth = 10.0
        ..strokeCap = StrokeCap.round
        ..style = PaintingStyle.stroke;

      final lineStart = tangent.position;
      final lineDirection = tangent.vector;

      final lineEnd = lineStart +
          Offset(
            lineDirection.dx * 30,
            lineDirection.dy * 30,
          );

       canvas.drawLine(lineStart, lineEnd, movingLinePaint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

i want that the line moving should follow the exact path as the outer rectangle is defined. i tried and tested different thing but not able to get the desired behaviour. how can i proceed further to achieve the desired functionality or behaviour or some relevant code snippet which can help me progress in the code.

in the image you can see when it reaches the curved path the line moving is not following the curveness, it simply takes a turn to right. this is the problem.

enter image description here

2

Answers


  1. Please try this code

    import 'package:flutter/material.dart';
    import 'dart:ui';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: '',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: AnimatedPathScreen(),
        );
      }
    }
    
    class AnimatedPathScreen extends StatefulWidget {
      @override
      _AnimatedPathScreenState createState() => _AnimatedPathScreenState();
    }
    
    class _AnimatedPathScreenState extends State<AnimatedPathScreen>
        with SingleTickerProviderStateMixin {
      late AnimationController _controller;
      late Animation<double> _animation;
    
      @override
      void initState() {
        super.initState();
    
        _controller = AnimationController(
          duration: const Duration(seconds: 5),
          vsync: this,
        )..repeat(reverse: false);
    
        _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Animated Path'),
          ),
          body: Center(
            child: CustomPaint(
              size: Size(300, 300),
              painter: AnimatedPathPainter(animation: _animation),
            ),
          ),
        );
      }
    }
    
    class AnimatedPathPainter extends CustomPainter {
      final Animation<double> animation;
      final double lineLength = 50.0; 
    
      AnimatedPathPainter({required this.animation}) : super(repaint: animation);
    
      @override
      void paint(Canvas canvas, Size size) {
        final paint = Paint()
          ..color = Colors.red
          ..strokeWidth = 4.0 
          ..style = PaintingStyle.stroke;
    
        final path = Path();
        final rect = Rect.fromLTWH(20, 20, size.width - 40, size.height - 40);
        path.addRRect(RRect.fromRectAndRadius(rect, Radius.circular(20)));
    
        final pathMetrics = path.computeMetrics().toList();
        final totalLength = pathMetrics.fold<double>(
          0.0,
          (double prev, PathMetric metric) => prev + metric.length,
        );
    
        double currentLength = (animation.value * totalLength) % totalLength;
        double startLength = currentLength;
        double endLength = (currentLength + lineLength) % totalLength;
    
        final pathSegment = Path();
    
        for (PathMetric metric in pathMetrics) {
          if (startLength < metric.length) {
            if (startLength < endLength) {
              pathSegment.addPath(metric.extractPath(startLength, endLength), Offset.zero);
            } else {
              pathSegment.addPath(metric.extractPath(startLength, metric.length), Offset.zero);
              pathSegment.addPath(metric.extractPath(0, endLength), Offset.zero);
            }
            break;
          } else {
            startLength -= metric.length;
            endLength -= metric.length;
          }
        }
    
        canvas.drawPath(pathSegment, paint);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
    }
    
    Login or Signup to reply.
  2. instead of pathMetric.getTangentForOffset use pathMetric.extractPath, notice that it has to be called twice when trying to get a sub-path at the end of the given path

    the complete code (note: timeDilation = 5; is used just for testing, your are free to delete it):

    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'dart:ui';
    
    import 'package:flutter/scheduler.dart';
    
    void main() => runApp(MaterialApp(home: Scaffold(body: Foo())));
    
    class Foo extends StatefulWidget {
      @override
      _FooState createState() => _FooState();
    }
    
    class _FooState extends State<Foo> with SingleTickerProviderStateMixin {
      late final _controller = AnimationController(vsync: this, duration: Durations.extralong4);
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            color: Colors.black,
            padding: const EdgeInsets.all(4),
            child: CustomPaint(
              painter: RoundedRectanglePainter(
                strokeColors: [Colors.green.shade900, Colors.green.shade700, Colors.blue.shade900, Colors.grey],
                strokeWidth: 12,
                borderRadius: 32,
                fillColors: [Colors.pink.shade700, Colors.grey.shade600, Colors.indigo.shade900],
                padding: 6,
                animation: _controller,
              ),
              child: Center(
                child: ElevatedButton(onPressed: () => _controller.forward(from: 0), child: const Text('animate')),
              ),
            ),
          ),
        );
      }
    
      @override
      void dispose() {
        _controller.dispose();
        super.dispose();
      }
    }
    
    class RoundedRectanglePainter extends CustomPainter {
      final double strokeWidth;
      final double borderRadius;
      final double padding;
      final Animation<double> animation;
      final Gradient strokeGradient;
      final Gradient fillGradient;
    
      RoundedRectanglePainter({
        required List<Color> strokeColors,
        required this.strokeWidth,
        required this.borderRadius,
        required List<Color> fillColors,
        this.padding = 10,
        required this.animation,
      }) :
        strokeGradient = LinearGradient(
          colors: strokeColors,
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        fillGradient = LinearGradient(
          colors: fillColors,
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
        super(repaint: animation);
    
      @override
      void paint(Canvas canvas, Size size) {
        // slow motion - just for testing
        timeDilation = 5;
    
        // rects
        final outerRect = (Offset.zero & size).deflate(strokeWidth / 2);
        final innerRect = outerRect.deflate(padding + strokeWidth / 2);
    
        // paints
        final strokePaint = Paint()
          ..shader = strokeGradient.createShader(outerRect)
          ..strokeWidth = strokeWidth
          ..style = PaintingStyle.stroke;
        final fillPaint = Paint()
          ..shader = fillGradient.createShader(innerRect)
          ..style = PaintingStyle.fill;
        final movingLinePaint = Paint()
          ..color = Colors.white
          ..strokeWidth = strokeWidth
          ..strokeCap = StrokeCap.round
          ..style = PaintingStyle.stroke;
    
        // round rects
        final outerRRect = RRect.fromRectAndRadius(outerRect, Radius.circular(borderRadius));
        final innerRRect = RRect.fromRectAndRadius(innerRect, Radius.circular(borderRadius - padding - strokeWidth / 2));
    
        // moving sub-path
        final outerPath = Path()..addRRect(outerRRect);
        final pathMetric = outerPath.computeMetrics().first;
        final start = pathMetric.length * animation.value;
        final end = start + borderRadius * pi * 0.5;
        final path = pathMetric.extractPath(start, end);
        if (end > pathMetric.length) {
          path.addPath(pathMetric.extractPath(0, end - pathMetric.length), Offset.zero);
        }
    
        // draw everything
        canvas
          ..drawRRect(outerRRect, strokePaint)
          ..drawRRect(innerRRect, fillPaint)
          ..drawPath(path, movingLinePaint);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search