image

19 Apr 2026

9K

35K

Flutter Animated Rotating Loader with Gradient and Bounce Effect

Creating engaging user interfaces often involves subtle animations that provide feedback and delight users. A custom loader is a perfect example, transforming a mundane waiting period into a more pleasant experience. This article will guide you through building a Flutter animated rotating loader with a vibrant gradient and a playful bounce effect.

Introduction

Loaders are essential UI components that indicate ongoing processes, preventing users from wondering if an application has frozen. While Flutter provides built-in progress indicators, a custom loader can significantly enhance an application's brand identity and user experience. We will combine several Flutter animation and painting concepts to achieve a visually appealing and dynamic loader:

  • AnimationController for managing animation state and duration.
  • Tween for defining animation ranges.
  • AnimatedBuilder for efficient widget rebuilding during animation.
  • Transform.rotate and Transform.scale for geometric transformations.
  • CustomPainter for drawing custom shapes, specifically an arc.
  • LinearGradient for adding color transitions to our loader.
  • Curves for defining non-linear animation effects, such as bouncing.

Core Concepts

AnimationController and Tweens

An AnimationController manages the animation. It requires a vsync object (typically the TickerProviderStateMixin) and a duration. Tweens define the start and end values of an animation. For rotation, we'll animate from 0 to radians. For scaling (bounce), we'll animate between 1.0 and a slightly larger value.

AnimatedBuilder

AnimatedBuilder is a highly optimized widget for animations. It rebuilds its child when an Animation object notifies its listeners. This allows us to separate the animation logic from the widget's build method, improving performance by only rebuilding the animated parts of the widget tree.

CustomPainter with LinearGradient

To draw the arc of our loader, we'll use a CustomPainter. This class provides a Canvas and a Size object, allowing us to draw directly onto the screen. We'll create a Paint object and assign a LinearGradient to its shader property to give the arc a dynamic color transition.

Transformations (Rotate and Scale)

Flutter's Transform widget allows us to apply various transformations like rotation, scaling, and translation. We'll use Transform.rotate driven by our rotation animation and Transform.scale for the bounce effect.

Implementation Steps

1. Setting up the Widget

We'll create a StatefulWidget called RotatingLoader to manage the animation controllers and state. It will need TickerProviderStateMixin for vsync.


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

class RotatingLoader extends StatefulWidget {
  final double size;
  final double strokeWidth;
  final Duration rotationDuration;
  final Duration bounceDuration;
  final List<Color> gradientColors;

  const RotatingLoader({
    Key? key,
    this.size = 100.0,
    this.strokeWidth = 8.0,
    this.rotationDuration = const Duration(seconds: 2),
    this.bounceDuration = const Duration(milliseconds: 700),
    this.gradientColors = const [Colors.blue, Colors.purple],
  }) : super(key: key);

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

class _RotatingLoaderState extends State<RotatingLoader> with TickerProviderStateMixin {
  late AnimationController _rotationController;
  late AnimationController _bounceController;
  late Animation<double> _rotationAnimation;
  late Animation<double> _bounceAnimation;

  @override
  void initState() {
    super.initState();

    _rotationController = AnimationController(
      vsync: this,
      duration: widget.rotationDuration,
    )..repeat(); // Repeat indefinitely for continuous rotation

    _rotationAnimation = Tween<double>(begin: 0, end: 2 * math.pi).animate(
      _rotationController,
    );

    _bounceController = AnimationController(
      vsync: this,
      duration: widget.bounceDuration,
    )..repeat(reverse: true); // Repeat with reverse for a pulsating bounce

    _bounceAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
      CurvedAnimation(
        parent: _bounceController,
        curve: Curves.elasticOut, // Or Curves.bounceOut for a different feel
      ),
    );
  }

  @override
  void dispose() {
    _rotationController.dispose();
    _bounceController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _rotationAnimation,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotationAnimation.value,
          child: AnimatedBuilder(
            animation: _bounceAnimation,
            builder: (context, child) {
              return Transform.scale(
                scale: _bounceAnimation.value,
                child: SizedBox(
                  width: widget.size,
                  height: widget.size,
                  child: CustomPaint(
                    painter: _LoaderPainter(
                      strokeWidth: widget.strokeWidth,
                      gradientColors: widget.gradientColors,
                    ),
                  ),
                ),
              );
            },
          ),
        );
      },
    );
  }
}

2. Creating the Custom Painter

The _LoaderPainter class will handle drawing the arc. It will receive the strokeWidth and gradientColors from the parent widget.


class _LoaderPainter extends CustomPainter {
  final double strokeWidth;
  final List<Color> gradientColors;

  _LoaderPainter({required this.strokeWidth, required this.gradientColors});

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
    final gradient = LinearGradient(
      colors: gradientColors,
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    );

    final paint = Paint()
      ..shader = gradient.createShader(rect)
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round; // Gives rounded ends to the arc

    // Draw an arc covering a segment of the circle.
    // Here, we draw from 0 radians (3 o'clock position) for 3/4 of a circle (1.5 * pi)
    canvas.drawArc(
      rect,
      0, // startAngle
      1.5 * math.pi, // sweepAngle (e.g., 270 degrees)
      false, // useCenter
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant _LoaderPainter oldDelegate) {
    return oldDelegate.strokeWidth != strokeWidth ||
           oldDelegate.gradientColors != gradientColors;
  }
}

Full Code Example

To put it all together, here's a complete main.dart file demonstrating how to use the RotatingLoader:


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

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Custom Loader',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.dark,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Custom Rotating Loader'),
      ),
      body: Center(
        child: RotatingLoader(
          size: 150.0,
          strokeWidth: 10.0,
          rotationDuration: const Duration(seconds: 3),
          bounceDuration: const Duration(seconds: 1),
          gradientColors: const [
            Color(0xFF8A2387), // Vivid Violet
            Color(0xFFE94057), // Sunset Orange
            Color(0xFFF27121), // Electric Orange
          ],
        ),
      ),
    );
  }
}

class RotatingLoader extends StatefulWidget {
  final double size;
  final double strokeWidth;
  final Duration rotationDuration;
  final Duration bounceDuration;
  final List<Color> gradientColors;

  const RotatingLoader({
    Key? key,
    this.size = 100.0,
    this.strokeWidth = 8.0,
    this.rotationDuration = const Duration(seconds: 2),
    this.bounceDuration = const Duration(milliseconds: 700),
    this.gradientColors = const [Colors.blue, Colors.purple],
  }) : super(key: key);

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

class _RotatingLoaderState extends State<RotatingLoader> with TickerProviderStateMixin {
  late AnimationController _rotationController;
  late AnimationController _bounceController;
  late Animation<double> _rotationAnimation;
  late Animation<double> _bounceAnimation;

  @override
  void initState() {
    super.initState();

    _rotationController = AnimationController(
      vsync: this,
      duration: widget.rotationDuration,
    )..repeat();

    _rotationAnimation = Tween<double>(begin: 0, end: 2 * math.pi).animate(
      _rotationController,
    );

    _bounceController = AnimationController(
      vsync: this,
      duration: widget.bounceDuration,
    )..repeat(reverse: true);

    _bounceAnimation = Tween<double>(begin: 1.0, end: 1.15).animate(
      CurvedAnimation(
        parent: _bounceController,
        curve: Curves.elasticOut,
      ),
    );
  }

  @override
  void dispose() {
    _rotationController.dispose();
    _bounceController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _rotationAnimation,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotationAnimation.value,
          child: AnimatedBuilder(
            animation: _bounceAnimation,
            builder: (context, child) {
              return Transform.scale(
                scale: _bounceAnimation.value,
                child: SizedBox(
                  width: widget.size,
                  height: widget.size,
                  child: CustomPaint(
                    painter: _LoaderPainter(
                      strokeWidth: widget.strokeWidth,
                      gradientColors: widget.gradientColors,
                    ),
                  ),
                ),
              );
            },
          ),
        );
      },
    );
  }
}

class _LoaderPainter extends CustomPainter {
  final double strokeWidth;
  final List<Color> gradientColors;

  _LoaderPainter({required this.strokeWidth, required this.gradientColors});

  @override
  void paint(Canvas canvas, Size size) {
    final rect = Rect.fromLTWH(0, 0, size.width, size.height);
    final gradient = LinearGradient(
      colors: gradientColors,
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
    );

    final paint = Paint()
      ..shader = gradient.createShader(rect)
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    canvas.drawArc(
      rect,
      0,
      1.5 * math.pi,
      false,
      paint,
    );
  }

  @override
  bool shouldRepaint(covariant _LoaderPainter oldDelegate) {
    return oldDelegate.strokeWidth != strokeWidth ||
           oldDelegate.gradientColors != gradientColors;
  }
}

Conclusion

We've successfully created a custom animated rotating loader in Flutter, featuring a vibrant gradient and an elastic bounce effect. This example demonstrates the power and flexibility of Flutter's animation framework, CustomPainter, and geometric transformations. By understanding these core concepts, you can design and implement a wide array of unique and engaging UI elements that truly differentiate your application.

Feel free to experiment with different gradient colors, sweep angles for the arc, animation durations, and `Curves` to create a loader that perfectly fits your application's aesthetic.

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