Creating Animated Custom Paintings in Flutter

Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, is known for its flexibility and powerful custom drawing capabilities. Creating animated custom paintings can significantly enhance the visual appeal and user experience of your apps. This article delves into how you can create animated custom paintings in Flutter with detailed examples.

What are Animated Custom Paintings in Flutter?

Animated custom paintings involve drawing on a Canvas using the CustomPaint widget in Flutter, with the drawings changing over time. These animations can range from simple transitions to complex, interactive visualizations, providing a unique and engaging UI.

Why Use Animated Custom Paintings?

  • Unique UI/UX: Offers the ability to create unique user interfaces and experiences beyond standard widgets.
  • Performance: Optimized drawing operations can provide better performance compared to complex widget compositions.
  • Flexibility: Allows precise control over the rendering process, ideal for graphs, charts, and visual effects.

How to Create Animated Custom Paintings in Flutter

Creating animated custom paintings involves these primary steps:

Step 1: Setting Up a Custom Painter Class

You need to create a custom painter class that extends CustomPainter. This class is where you define how to draw on the canvas.


import 'package:flutter/material.dart';
import 'dart:math';

class AnimatedCirclePainter extends CustomPainter {
  final Animation<double> animation;

  AnimatedCirclePainter({required this.animation}) : super(repaint: animation);

  @override
  void paint(Canvas canvas, Size size) {
    final center = size.center(Offset.zero);
    final radius = size.width / 3 * animation.value;

    final paint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.fill;

    canvas.drawCircle(center, radius, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Explanation:

  • AnimatedCirclePainter extends CustomPainter and takes an Animation<double> as input.
  • The paint method defines how to draw the circle, with the radius changing based on the animation.value.
  • super(repaint: animation) ensures that the painter repaints whenever the animation value changes.
  • shouldRepaint is overridden to optimize the painting process. In this example, we return false because the repaint is handled by the animation.

Step 2: Integrating the Custom Painter into a Widget

Integrate the custom painter using the CustomPaint widget in your Flutter app.


import 'package:flutter/material.dart';

class AnimatedCircle extends StatefulWidget {
  @override
  _AnimatedCircleState createState() => _AnimatedCircleState();
}

class _AnimatedCircleState extends State<AnimatedCircle> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _controller.repeat(reverse: true);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: CustomPaint(
        size: Size(200, 200),
        painter: AnimatedCirclePainter(animation: _animation),
      ),
    );
  }
}

Explanation:

  • AnimatedCircle is a stateful widget that manages the animation controller.
  • An AnimationController is initialized in initState with a duration of 2 seconds.
  • The Tween creates an animation that ranges from 0.0 to 1.0.
  • _controller.repeat(reverse: true) makes the animation loop back and forth.
  • The CustomPaint widget uses the AnimatedCirclePainter and passes in the _animation.

Step 3: Run the App

Add the AnimatedCircle widget to your main.dart file or any part of your application.


import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Animated Circle'),
        ),
        body: Center(
          child: AnimatedCircle(),
        ),
      ),
    ),
  );
}

Now, when you run your Flutter app, you will see an animated circle that grows and shrinks.

Example: Animated Line Graph

Let’s create a more complex animated custom painting example: an animated line graph.

Step 1: Custom Painter Class


import 'package:flutter/material.dart';

class AnimatedLineGraphPainter extends CustomPainter {
  final Animation<double> animation;
  final List<double> dataPoints;

  AnimatedLineGraphPainter({required this.animation, required this.dataPoints})
      : super(repaint: animation);

  @override
  void paint(Canvas canvas, Size size) {
    if (dataPoints.isEmpty) return;

    final paint = Paint()
      ..color = Colors.green
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2.0;

    final path = Path();

    final double xIncrement = size.width / (dataPoints.length - 1);
    final double maxDataPoint = dataPoints.reduce(max);
    final double heightScale = size.height / maxDataPoint;

    for (int i = 0; i < dataPoints.length; i++) {
      final double x = i * xIncrement;
      final double y = size.height - dataPoints[i] * heightScale;

      if (i == 0) {
        path.moveTo(x, y);
      } else {
        path.lineTo(x, y);
      }
    }

    // Animate the path
    final animatedPath = createAnimatedPath(path, animation.value);

    canvas.drawPath(animatedPath, paint);
  }

  Path createAnimatedPath(Path originalPath, double animationProgress) {
    final animatedPath = Path();
    final pathMetrics = originalPath.computeMetrics();

    for (PathMetric pathMetric in pathMetrics) {
      final pathLength = pathMetric.length;
      final extractLength = pathLength * animationProgress;
      if (extractLength == 0.0) return animatedPath;

      final partialPath = pathMetric.extractPath(0.0, extractLength, startWithMoveTo: true);
      animatedPath.addPath(partialPath, Offset.zero);
    }

    return animatedPath;
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Explanation:

  • AnimatedLineGraphPainter extends CustomPainter and requires an Animation<double> and a list of dataPoints.
  • The paint method draws a line graph based on the dataPoints.
  • The method createAnimatedPath uses path metrics to progressively reveal the path, creating an animation effect.

Step 2: Integrating the Custom Painter into a Widget


import 'package:flutter/material.dart';
import 'animated_line_graph_painter.dart';

class AnimatedLineGraph extends StatefulWidget {
  final List<double> dataPoints;

  AnimatedLineGraph({required this.dataPoints});

  @override
  _AnimatedLineGraphState createState() => _AnimatedLineGraphState();
}

class _AnimatedLineGraphState extends State<AnimatedLineGraph> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(300, 200),
      painter: AnimatedLineGraphPainter(animation: _animation, dataPoints: widget.dataPoints),
    );
  }
}

Explanation:

  • AnimatedLineGraph is a stateful widget that takes a list of dataPoints as input.
  • The AnimationController is set up to animate from 0.0 to 1.0 over 3 seconds.
  • The CustomPaint widget renders the AnimatedLineGraphPainter with the _animation and dataPoints.

Step 3: Run the App


import 'package:flutter/material.dart';
import 'animated_line_graph.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Animated Line Graph'),
        ),
        body: Center(
          child: AnimatedLineGraph(dataPoints: [10, 30, 20, 60, 40, 80, 50]),
        ),
      ),
    ),
  );
}

With these steps, you’ll have an animated line graph that draws itself smoothly from start to end.

Tips for Optimization

  • Use shouldRepaint: Implement the shouldRepaint method to prevent unnecessary redraws.
  • Minimize Calculations: Perform calculations outside of the paint method to improve performance.
  • Layered Canvases: Use multiple CustomPaint widgets layered on top of each other to isolate different parts of the drawing.

Conclusion

Animated custom paintings in Flutter provide an effective way to create visually appealing and highly customized user interfaces. By understanding the principles of CustomPainter, AnimationController, and how to efficiently draw on the Canvas, you can create unique and engaging animations that elevate the user experience of your Flutter applications.