image

29 Dec 2025

9K

35K

Custom Painter Animations in Flutter for Dynamic UIs

Introduction

Flutter's rendering engine provides incredible flexibility, allowing developers to create highly customized and pixel-perfect user interfaces. While standard widgets cover a vast range of UI needs, there are instances where unique visual elements, complex data visualizations, or bespoke animations require a more granular approach. This is where CustomPainter shines. When combined with Flutter's animation framework, CustomPainter transforms static drawings into dynamic, engaging experiences, making it a powerful tool for crafting truly distinctive UIs.

Understanding CustomPainter

At its core, CustomPainter is an abstract class that allows you to draw directly onto the canvas using various drawing primitives such as lines, circles, arcs, paths, and text. You implement two main methods:

  • paint(Canvas canvas, Size size): This is where all your drawing logic resides. The Canvas object provides methods for drawing, and the Size object tells you the available drawing area.
  • shouldRepaint(covariant CustomPainter oldDelegate): This method is crucial for performance. It determines whether the painter needs to redraw. If your drawing depends on external data, this method should compare the new data with the old data and return true if a repaint is necessary, otherwise false.

While CustomPainter provides the means to draw, by itself, it creates static images. To bring these drawings to life, we need to integrate them with Flutter's animation system.

Bringing CustomPainter to Life with Animation

Animating a CustomPainter involves updating the data that the painter uses for drawing over time. Flutter's animation framework provides the tools to manage this temporal aspect:

  • AnimationController: This controls the progress of an animation. It can be forwarded, reversed, repeated, and stopped. It produces values typically ranging from 0.0 to 1.0.
  • Tween: A Tween (short for "between") defines the range of values an animation can animate between (e.g., from 0 to 360 degrees, or from a small radius to a large one). It interpolates values based on the AnimationController's progress.
  • AnimatedBuilder: This widget listens to an Animation and rebuilds its child subtree whenever the animation's value changes. This is a highly efficient way to update only the parts of the UI that depend on the animation, preventing unnecessary rebuilds of the entire widget tree.

The general approach is to use an AnimationController to drive a Tween, and then pass the animated value from the Tween to your CustomPainter. The AnimatedBuilder ensures that the CustomPaint widget (which uses your CustomPainter) is rebuilt whenever the animation updates, triggering the paint method with the new animated values.

A Practical Example: Animating a Custom Wave

Let's illustrate this with an example: creating a continuously moving and evolving wave animation. We'll use CustomPainter to draw a sine wave and animate its phase shift to create a fluid motion.

Code Implementation

1. The AnimatedWave Widget

This widget will manage the AnimationController and use an AnimatedBuilder to redraw the CustomPaint widget as the animation progresses.


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

class AnimatedWave extends StatefulWidget {
  const AnimatedWave({super.key});

  @override
  State<AnimatedWave> createState() => _AnimatedWaveState();
}

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

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 4), // Duration for one full cycle
    )..repeat(); // Repeat the animation indefinitely

    _wavePhaseAnimation = Tween<double>(begin: 0.0, end: 2 * math.pi).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Animated Wave with CustomPainter'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _wavePhaseAnimation,
          builder: (context, child) {
            return CustomPaint(
              size: const Size(300, 200), // Fixed size for the wave container
              painter: WavePainter(
                wavePhase: _wavePhaseAnimation.value,
                amplitude: 30.0, // Adjust wave height
                frequency: 1.0, // Adjust number of waves
                color: Colors.blue.shade700,
              ),
            );
          },
        ),
      ),
    );
  }

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

2. The WavePainter Class

This CustomPainter will take the animated wavePhase value and use it to draw a dynamic sine wave.


class WavePainter extends CustomPainter {
  final double wavePhase;
  final double amplitude;
  final double frequency;
  final Color color;

  WavePainter({
    required this.wavePhase,
    this.amplitude = 20.0,
    this.frequency = 1.0,
    this.color = Colors.blue,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint()
      ..color = color
      ..style = PaintingStyle.fill; // Use fill to draw a solid wave

    final Path path = Path();
    path.moveTo(0, size.height / 2); // Start from the middle left

    for (double i = 0.0; i <= size.width; i++) {
      // Calculate y-coordinate using sine wave formula
      // y = amplitude * sin( (x / width * 2 * pi * frequency) + phase) + center_y
      final double y = amplitude * math.sin(
        (i / size.width * 2 * math.pi * frequency) + wavePhase
      ) + size.height / 2;
      path.lineTo(i, y);
    }

    path.lineTo(size.width, size.height); // Bottom right
    path.lineTo(0, size.height); // Bottom left
    path.close(); // Close the path to form a shape

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant WavePainter oldDelegate) {
    // Only repaint if the wave phase or other properties change
    return oldDelegate.wavePhase != wavePhase ||
           oldDelegate.amplitude != amplitude ||
           oldDelegate.frequency != frequency ||
           oldDelegate.color != color;
  }
}

Explanation of the Code

  • _AnimatedWaveState:
    • It uses SingleTickerProviderStateMixin to provide a Ticker for the AnimationController.
    • _controller is initialized to run for 4 seconds and then repeat indefinitely.
    • _wavePhaseAnimation is a Tween that interpolates double values from 0.0 to 2 * math.pi (a full cycle in radians), driven by _controller.
    • The build method uses an AnimatedBuilder. This widget listens to _wavePhaseAnimation. Whenever _wavePhaseAnimation.value changes, the builder callback is invoked, which in turn rebuilds the CustomPaint widget with a new WavePainter instance.
    • The current value of _wavePhaseAnimation is passed as wavePhase to the WavePainter.
    • dispose ensures the _controller is released when the widget is removed from the tree to prevent memory leaks.
  • WavePainter:
    • Its constructor accepts wavePhase, amplitude, frequency, and color, allowing these properties to be dynamic.
    • In the paint method, a Path is generated. The `y`-coordinates for the wave are calculated using the sine function: `amplitude * sin((x_position / width * 2 * pi * frequency) + wavePhase) + center_y`.
    • The `wavePhase` parameter, which is animated, effectively shifts the starting point of the sine wave, creating the illusion of horizontal motion.
    • The path is then closed and filled using the specified `color`.
    • shouldRepaint returns true if any of its drawing-related properties (wavePhase, amplitude, frequency, color) have changed, ensuring that redrawing only happens when necessary, optimizing performance.

Benefits and Use Cases

Utilizing CustomPainter with animation offers several significant advantages:

  • Unconstrained Creativity: Design and implement any visual effect, chart, graph, or custom loader that might not be possible with standard widgets.
  • High Performance: When implemented correctly with shouldRepaint and AnimatedBuilder, CustomPainter can be extremely performant as it operates at a low level, directly on the canvas.
  • Pixel-Perfect Control: Achieve precise control over every pixel, essential for branding, data visualization, or complex game UIs.
  • Dynamic Data Visualization: Create animated charts, graphs, and indicators that react to real-time data changes.
  • Unique UI Elements: Craft custom sliders, toggles, progress indicators, or splash screens that stand out.

Conclusion

Flutter's CustomPainter, when paired with its robust animation framework, unlocks an unparalleled level of control and creativity for UI development. By understanding how to manipulate drawing parameters with animated values and efficiently update the canvas using AnimatedBuilder, developers can transform static designs into dynamic, engaging, and high-performance user experiences. This powerful combination empowers you to build UIs that are not just functional, but truly captivating and unique.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is