Implementing Custom Animations using CustomPainter and Canvas in Flutter

Flutter is renowned for its rich set of animation capabilities, allowing developers to create smooth and engaging user experiences. While Flutter’s pre-built animation widgets are powerful, custom animations offer even greater flexibility and control. This blog post will guide you through implementing custom animations in Flutter using CustomPainter and Canvas, providing you with the tools to bring unique and complex designs to life.

What are Custom Animations?

Custom animations are animations created from scratch, tailored to fit specific needs or unique design concepts. Unlike pre-built animations, custom animations allow developers to define every aspect of the animation, providing maximum control over its appearance and behavior.

Why Use Custom Animations?

  • Flexibility: Create animations that go beyond standard effects.
  • Uniqueness: Develop distinctive, brand-specific animations.
  • Control: Fine-tune every detail of the animation.

Understanding CustomPainter and Canvas

To create custom animations, we utilize two key classes:

  • CustomPainter: A class that allows you to paint custom graphics onto the screen. You override its paint method to draw whatever you want.
  • Canvas: An object provided within the paint method of CustomPainter, which offers a surface for drawing shapes, text, and images.

How to Implement Custom Animations

Here’s a step-by-step guide on implementing custom animations using CustomPainter and Canvas in Flutter.

Step 1: Create a StatefulWidget

First, create a StatefulWidget to manage the animation state. This widget will hold the animation controller and trigger redraws.

import 'package:flutter/material.dart';

class CustomAnimation extends StatefulWidget {
  @override
  _CustomAnimationState createState() => _CustomAnimationState();
}

class _CustomAnimationState extends State<CustomAnimation> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    )..repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Custom Animation Example'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return CustomPaint(
              size: Size(300, 300), // Adjust size as needed
              painter: MyPainter(_controller.value),
            );
          },
        ),
      ),
    );
  }
}

Key elements in this step:

  • AnimationController: Manages the animation timeline. The vsync: this ensures the animation runs smoothly.
  • AnimatedBuilder: Listens to the animation controller and rebuilds the UI on every tick.
  • CustomPaint: A widget that uses CustomPainter to draw custom graphics.

Step 2: Create a CustomPainter Class

Next, create a class that extends CustomPainter. Override the paint method to draw your custom animation using the Canvas object.

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

class MyPainter extends CustomPainter {
  final double progress;

  MyPainter(this.progress);

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

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

    final angle = 2 * math.pi * progress;

    final x = center.dx + radius * math.cos(angle);
    final y = center.dy + radius * math.sin(angle);

    canvas.drawCircle(Offset(x, y), 20, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true; // Always repaint for animation
  }
}

In this example:

  • The paint method calculates the position of a circle based on the progress of the animation.
  • canvas.drawCircle draws a circle at the calculated position.
  • The shouldRepaint method returns true, indicating that the painter should repaint whenever the animation progress changes.

Step 3: Run Your Animation

Now, run the CustomAnimation widget in your Flutter app. The circle will move in a circular path, creating a simple custom animation.

void main() {
  runApp(
    MaterialApp(
      home: CustomAnimation(),
    ),
  );
}

Advanced Custom Animations

You can create more complex animations by combining multiple drawing operations, adjusting colors, and adding easing functions. Here are some ideas:

  • Drawing Shapes: Use canvas.drawRect, canvas.drawPath, and other methods to draw various shapes.
  • Gradient Colors: Apply gradient colors to your shapes using Paint.
  • Transforms: Use canvas.translate, canvas.rotate, and canvas.scale to transform your drawings.
  • Complex Paths: Create intricate paths using the Path class for unique shapes.

Example: Animated Wave

Let’s create an animated wave effect using CustomPainter.

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

class WavePainter extends CustomPainter {
  final double progress;

  WavePainter(this.progress);

  @override
  void paint(Canvas canvas, Size size) {
    final wavePaint = Paint()
      ..color = Colors.blue.withOpacity(0.5)
      ..style = PaintingStyle.fill;

    final path = Path();
    path.moveTo(0, size.height / 2);

    final waveHeight = 20.0;
    final waveWidth = 100.0;

    for (double i = 0; i < size.width; i++) {
      final y = size.height / 2 + math.sin((i / waveWidth - progress) * math.pi * 2) * waveHeight;
      path.lineTo(i, y);
    }

    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);
    path.close();

    canvas.drawPath(path, wavePaint);
  }

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

class AnimatedWave extends StatefulWidget {
  @override
  _AnimatedWaveState createState() => _AnimatedWaveState();
}

class _AnimatedWaveState extends State<AnimatedWave> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 3),
    )..repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Animated Wave Example'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return CustomPaint(
              size: Size(300, 200),
              painter: WavePainter(_controller.value),
            );
          },
        ),
      ),
    );
  }
}

This example creates a wave animation by drawing a path that moves horizontally based on the animation progress.

Best Practices for Custom Animations

  • Performance: Optimize your paint method to avoid unnecessary calculations.
  • Readability: Break down complex animations into smaller, manageable functions.
  • Reusability: Design your CustomPainter classes to be reusable and configurable.
  • State Management: Properly manage the animation state to ensure smooth transitions.

Conclusion

Implementing custom animations using CustomPainter and Canvas in Flutter opens up a world of creative possibilities. By mastering these tools, you can craft unique and engaging animations that enhance the user experience of your Flutter applications. Experiment with different shapes, colors, and transformations to create stunning visual effects.