Back to Posts

Advanced Flutter Animations and Transitions

17 min read

Flutter provides a powerful animation system that can create beautiful and engaging user experiences. In this post, we'll explore advanced animation techniques and transitions that can take your Flutter apps to the next level.

1. Custom Animation Controllers

Create complex animations with custom controllers.

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

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

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

    _animation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );

    _controller.repeat(reverse: true);
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Transform.scale(
          scale: 1.0 + (_animation.value * 0.2),
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        );
      },
    );
  }

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

2. Staggered Animations

Create sequences of animations that follow each other.

class StaggeredAnimation extends StatelessWidget {
  final Animation<double> controller;
  final Animation<double> opacity;
  final Animation<double> width;
  final Animation<double> height;
  final Animation<EdgeInsets> padding;
  final Animation<BorderRadius> borderRadius;
  final Animation<Color> color;

  StaggeredAnimation({Key? key, required this.controller})
      : opacity = Tween<double>(
          begin: 0.0,
          end: 1.0,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.0, 0.1, curve: Curves.ease),
        )),
        width = Tween<double>(
          begin: 50.0,
          end: 150.0,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.1, 0.3, curve: Curves.ease),
        )),
        height = Tween<double>(
          begin: 50.0,
          end: 150.0,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.3, 0.6, curve: Curves.ease),
        )),
        padding = EdgeInsetsTween(
          begin: const EdgeInsets.only(bottom: 16),
          end: const EdgeInsets.only(bottom: 75),
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.6, 0.8, curve: Curves.ease),
        )),
        borderRadius = BorderRadiusTween(
          begin: BorderRadius.circular(4),
          end: BorderRadius.circular(75),
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.8, 1.0, curve: Curves.ease),
        )),
        color = ColorTween(
          begin: Colors.blue,
          end: Colors.orange,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.0, 1.0, curve: Curves.ease),
        )),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, child) {
        return Container(
          padding: padding.value,
          alignment: Alignment.bottomCenter,
          child: Opacity(
            opacity: opacity.value,
            child: Container(
              width: width.value,
              height: height.value,
              decoration: BoxDecoration(
                color: color.value,
                border: Border.all(
                  color: Colors.white,
                  width: 3.0,
                ),
                borderRadius: borderRadius.value,
              ),
            ),
          ),
        );
      },
    );
  }
}

3. Page Route Transitions

Create custom page transitions for navigation.

class CustomPageRoute extends PageRouteBuilder {
  final Widget child;
  final AxisDirection direction;

  CustomPageRoute({
    required this.child,
    this.direction = AxisDirection.right,
  }) : super(
          transitionDuration: const Duration(milliseconds: 500),
          reverseTransitionDuration: const Duration(milliseconds: 500),
          pageBuilder: (context, animation, secondaryAnimation) => child,
        );

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: _getOffset(),
        end: Offset.zero,
      ).animate(CurvedAnimation(
        parent: animation,
        curve: Curves.easeOut,
      )),
      child: child,
    );
  }

  Offset _getOffset() {
    switch (direction) {
      case AxisDirection.up:
        return const Offset(0, 1);
      case AxisDirection.right:
        return const Offset(-1, 0);
      case AxisDirection.down:
        return const Offset(0, -1);
      case AxisDirection.left:
        return const Offset(1, 0);
    }
  }
}

4. Implicit Animations

Use built-in implicit animations for simple transitions.

class ImplicitAnimation extends StatefulWidget {
  @override
  _ImplicitAnimationState createState() => _ImplicitAnimationState();
}

class _ImplicitAnimationState extends State<ImplicitAnimation> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        width: _isExpanded ? 200 : 100,
        height: _isExpanded ? 200 : 100,
        decoration: BoxDecoration(
          color: _isExpanded ? Colors.orange : Colors.blue,
          borderRadius: BorderRadius.circular(_isExpanded ? 20 : 10),
        ),
        child: Center(
          child: AnimatedDefaultTextStyle(
            duration: const Duration(milliseconds: 300),
            style: TextStyle(
              fontSize: _isExpanded ? 24 : 16,
              color: Colors.white,
            ),
            child: const Text('Tap me!'),
          ),
        ),
      ),
    );
  }
}

5. Physics-based Animations

Create realistic animations using physics simulations.

class PhysicsAnimation extends StatefulWidget {
  @override
  _PhysicsAnimationState createState() => _PhysicsAnimationState();
}

class _PhysicsAnimationState extends State<PhysicsAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late SpringSimulation _simulation;
  double _position = 0.0;

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

    const spring = SpringDescription(
      mass: 1.0,
      stiffness: 500.0,
      damping: 10.0,
    );

    _simulation = SpringSimulation(spring, 0.0, 1.0, 0.0);

    _controller.addListener(() {
      setState(() {
        _position = _simulation.x(_controller.value);
      });
    });
  }

  void _startAnimation() {
    _controller.animateWith(_simulation);
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _startAnimation,
      child: Transform.translate(
        offset: Offset(0, 100 * _position),
        child: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            color: Colors.blue,
            shape: BoxShape.circle,
          ),
        ),
      ),
    );
  }

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

6. Hero Animations with Custom Transitions

Create custom hero animations with shared elements.

class CustomHeroAnimation extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: 'hero-tag',
      flightShuttleBuilder: (
        BuildContext flightContext,
        Animation<double> animation,
        HeroFlightDirection flightDirection,
        BuildContext fromHeroContext,
        BuildContext toHeroContext,
      ) {
        return AnimatedBuilder(
          animation: animation,
          builder: (context, child) {
            return Transform.scale(
              scale: 1.0 + (animation.value * 0.2),
              child: child,
            );
          },
          child: Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          ),
        );
      },
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    );
  }
}

7. Custom Tween Animations

class ColorSequenceTween extends Tween<Color?> {
  final List<Color> colors;
  final List<double> stops;

  ColorSequenceTween({
    required this.colors,
    required this.stops,
  }) : assert(colors.length == stops.length),
       super(begin: colors.first, end: colors.last);

  @override
  Color? lerp(double t) {
    if (t <= stops.first) return colors.first;
    if (t >= stops.last) return colors.last;

    for (int i = 0; i < stops.length - 1; i++) {
      if (t >= stops[i] && t <= stops[i + 1]) {
        final localT = (t - stops[i]) / (stops[i + 1] - stops[i]);
        return Color.lerp(colors[i], colors[i + 1], localT);
      }
    }
    return colors.last;
  }
}

class CustomTweenAnimation extends StatefulWidget {
  @override
  _CustomTweenAnimationState createState() => _CustomTweenAnimationState();
}

class _CustomTweenAnimationState extends State<CustomTweenAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Color?> _colorAnimation;

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

    _colorAnimation = ColorSequenceTween(
      colors: [
        Colors.blue,
        Colors.purple,
        Colors.red,
        Colors.orange,
        Colors.yellow,
      ],
      stops: [0.0, 0.25, 0.5, 0.75, 1.0],
    ).animate(_controller);

    _controller.repeat();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _colorAnimation,
      builder: (context, child) {
        return Container(
          width: 200,
          height: 200,
          decoration: BoxDecoration(
            color: _colorAnimation.value,
            borderRadius: BorderRadius.circular(20),
          ),
        );
      },
    );
  }

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

8. Performance Optimization

8.1 RepaintBoundary Usage

class OptimizedAnimation extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: CustomPaint(
        painter: AnimationPainter(),
      ),
    );
  }
}

class AnimationPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // Complex painting operations
  }

  @override
  bool shouldRepaint(AnimationPainter oldDelegate) {
    // Only repaint when necessary
    return false;
  }
}

8.2 Caching Animations

class CachedAnimation extends StatefulWidget {
  @override
  _CachedAnimationState createState() => _CachedAnimationState();
}

class _CachedAnimationState extends State<CachedAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<Widget> _cachedFrames = [];
  static const int totalFrames = 60;

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

    // Pre-compute animation frames
    for (int i = 0; i < totalFrames; i++) {
      final t = i / (totalFrames - 1);
      _cachedFrames.add(
        Transform.rotate(
          angle: 2 * pi * t,
          child: Container(
            width: 100,
            height: 100,
            decoration: BoxDecoration(
              gradient: LinearGradient(
                colors: [Colors.blue, Colors.purple],
                stops: [0, t],
              ),
            ),
          ),
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        final frame = (_controller.value * (totalFrames - 1)).round();
        return _cachedFrames[frame];
      },
    );
  }

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

Best Practices for Animations

  1. Always dispose of animation controllers
  2. Use CurvedAnimation for natural movement
  3. Consider performance implications
  4. Test animations on different devices
  5. Provide fallbacks for users who prefer reduced motion

Conclusion

Flutter's animation system is powerful and flexible, allowing you to create engaging user experiences. Remember to:

  • Keep animations subtle and purposeful
  • Consider performance implications
  • Test on various devices
  • Follow platform guidelines
  • Provide accessibility options

Stay tuned for more advanced animation techniques and real-world examples!