skills/flutter-ui-ux/references/animation-patterns.md

15 KiB

Flutter Animation Patterns

Implicit Animations

Simple animations with minimal code.

1. AnimatedContainer

Smooth property transitions.

class AnimatedCard extends StatefulWidget {
  final bool isSelected;
  final Widget child;

  const AnimatedCard({required this.isSelected, required this.child});

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

class _AnimatedCardState extends State<AnimatedCard> {
  bool _isHovered = false;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => setState(() => _isHovered = true),
      onExit: (_) => setState(() => _isHovered = false),
      child: AnimatedContainer(
        duration: Duration(milliseconds: 200),
        curve: Curves.easeInOut,
        width: _isHovered ? 200 : 180,
        height: _isHovered ? 120 : 100,
        decoration: BoxDecoration(
          color: widget.isSelected
            ? Theme.of(context).primaryColor
            : (_isHovered ? Colors.grey[200] : Colors.grey[100]),
          borderRadius: BorderRadius.circular(_isHovered ? 16 : 8),
          boxShadow: _isHovered
            ? [BoxShadow(color: Colors.black26, blurRadius: 8, offset: Offset(0, 4))]
            : [BoxShadow(color: Colors.black12, blurRadius: 2, offset: Offset(0, 1))],
        ),
        child: widget.child,
      ),
    );
  }
}

2. AnimatedOpacity & AnimatedScale

Fade and scale effects.

class FadeInButton extends StatefulWidget {
  final String text;
  final VoidCallback onPressed;
  final Duration delay;

  const FadeInButton({
    required this.text,
    required this.onPressed,
    this.delay = Duration.zero,
  });

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

class _FadeInButtonState extends State<FadeInButton> {
  bool _isVisible = false;

  @override
  void initState() {
    super.initState();
    Future.delayed(widget.delay, () {
      if (mounted) setState(() => _isVisible = true);
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: _isVisible ? 1.0 : 0.0,
      duration: Duration(milliseconds: 600),
      curve: Curves.easeOut,
      child: AnimatedScale(
        scale: _isVisible ? 1.0 : 0.8,
        duration: Duration(milliseconds: 600),
        curve: Curves.elasticOut,
        child: ElevatedButton(
          onPressed: widget.onPressed,
          child: Text(widget.text),
        ),
      ),
    );
  }
}

Explicit Animations

Fine-grained control with AnimationController.

1. Custom Tween Animation

Controlled property animation.

class AnimatedProgress extends StatefulWidget {
  final double progress; // 0.0 to 1.0
  final Color color;
  final Duration duration;

  const AnimatedProgress({
    required this.progress,
    this.color = Colors.blue,
    this.duration = const Duration(milliseconds: 800),
  });

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

class _AnimatedProgressState extends State<AnimatedProgress>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    );

    _animation = Tween<double>(
      begin: 0.0,
      end: widget.progress,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOutCubic,
    ));

    _controller.forward();
  }

  @override
  void didUpdateWidget(AnimatedProgress oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.progress != widget.progress) {
      _animation = Tween<double>(
        begin: _controller.value,
        end: widget.progress,
      ).animate(CurvedAnimation(
        parent: _controller,
        curve: Curves.easeInOutCubic,
      ));
      _controller.forward(from: _controller.value);
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return CustomPaint(
          painter: ProgressPainter(_animation.value, widget.color),
          child: child,
        );
      },
      child: Container(),
    );
  }
}

class ProgressPainter extends CustomPainter {
  final double progress;
  final Color color;

  ProgressPainter(this.progress, this.color);

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

    // Background
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromLTWH(0, 0, size.width, size.height),
        Radius.circular(size.height / 2),
      ),
      paint,
    );

    // Progress
    paint.color = color;
    canvas.drawRRect(
      RRect.fromRectAndRadius(
        Rect.fromLTWH(0, 0, size.width * progress, size.height),
        Radius.circular(size.height / 2),
      ),
      paint,
    );
  }

  @override
  bool shouldRepaint(ProgressPainter oldDelegate) {
    return oldDelegate.progress != progress || oldDelegate.color != color;
  }
}

2. Staggered Animation

Sequential animations with delays.

class StaggeredAnimationDemo extends StatefulWidget {
  final List<Widget> children;

  const StaggeredAnimationDemo({required this.children});

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

class _StaggeredAnimationDemoState extends State<AnimatedListDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late List<Animation<double>> _animations;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 1200),
      vsync: this,
    );

    _animations = List.generate(
      widget.children.length,
      (index) => Tween<double>(
        begin: 0.0,
        end: 1.0,
      ).animate(CurvedAnimation(
        parent: _controller,
        curve: Interval(
          index * 0.1, // Start delay
          0.5 + (index * 0.1), // End delay
          curve: Curves.easeOutCubic,
        ),
      )),
    );

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: List.generate(
        widget.children.length,
        (index) => AnimatedBuilder(
          animation: _animations[index],
          builder: (context, child) {
            return Transform.translate(
              offset: Offset(0, 50 * (1 - _animations[index].value)),
              child: Opacity(
                opacity: _animations[index].value,
                child: child,
              ),
            );
          },
          child: widget.children[index],
        ),
      ),
    );
  }
}

Physics Animations

Realistic motion with physics simulation.

1. Spring Animation

Bouncy, springy effects.

class SpringButton extends StatefulWidget {
  final Widget child;
  final VoidCallback onPressed;

  const SpringButton({required this.child, required this.onPressed});

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

class _SpringButtonState extends State<SpringButton>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 500),
      vsync: this,
    );

    _scaleAnimation = Tween<double>(
      begin: 1.0,
      end: 0.95,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.elasticIn,
    ));
  }

  void _handleTap() {
    _controller.forward().then((_) {
      _controller.reverse();
    });
    widget.onPressed();
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: AnimatedBuilder(
        animation: _scaleAnimation,
        builder: (context, child) {
          return Transform.scale(
            scale: _scaleAnimation.value,
            child: child,
          );
        },
        child: widget.child,
      ),
    );
  }
}

2. Draggable Card

Physics-based dragging.

class DraggableCard extends StatefulWidget {
  final Widget child;
  final VoidCallback? onSwipeComplete;

  const DraggableCard({required this.child, this.onSwipeComplete});

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

class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Offset> _slideAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 300),
      vsync: this,
    );

    _slideAnimation = Tween<Offset>(
      begin: Offset.zero,
      end: Offset(1.5, 0), // Swipe off screen
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutCubic,
    ));
  }

  void _handlePanUpdate(DragUpdateDetails details) {
    // Handle drag update
  }

  void _handlePanEnd(DragEndDetails details) {
    if (details.velocity.pixelsPerSecond.dx > 300) {
      // Swipe right
      _controller.forward().then((_) {
        widget.onSwipeComplete?.call();
      });
    } else {
      // Snap back
      _controller.reverse();
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: _handlePanUpdate,
      onPanEnd: _handlePanEnd,
      child: AnimatedBuilder(
        animation: _slideAnimation,
        builder: (context, child) {
          return SlideTransition(
            position: _slideAnimation,
            child: child,
          );
        },
        child: widget.child,
      ),
    );
  }
}

Hero Animations

Screen transitions with shared elements.

1. Hero Navigation

Smooth transitions between screens.

// First screen
class ProductListScreen extends StatelessWidget {
  final List<Product> products;

  const ProductListScreen({required this.products});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Products')),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          final product = products[index];
          return GestureDetector(
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => ProductDetailScreen(product: product),
                ),
              );
            },
            child: Hero(
              tag: 'product_${product.id}',
              child: ProductCard(product: product),
            ),
          );
        },
      ),
    );
  }
}

// Second screen
class ProductDetailScreen extends StatelessWidget {
  final Product product;

  const ProductDetailScreen({required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            expandedHeight: 300,
            flexibleSpace: FlexibleSpaceBar(
              background: Hero(
                tag: 'product_${product.id}',
                child: Image.network(
                  product.imageUrl,
                  fit: BoxFit.cover,
                ),
              ),
            ),
          ),
          SliverToBoxAdapter(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product.name,
                    style: Theme.of(context).textTheme.headlineMedium,
                  ),
                  SizedBox(height: 8),
                  Text(product.description),
                  // ... more content
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

2. Custom Hero Animation

Custom transition effects.

class CustomHero extends StatefulWidget {
  final Widget child;
  final String tag;
  final Duration duration;

  const CustomHero({
    required this.child,
    required this.tag,
    this.duration = const Duration(milliseconds: 600),
  });

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

class _CustomHeroState extends State<CustomHero>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;
  late Animation<double> _fadeAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    );

    _scaleAnimation = Tween<double>(
      begin: 0.8,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.elasticOut,
    ));

    _fadeAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.3, 1.0, curve: Curves.easeOut),
    ));

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnimation.value,
          child: Opacity(
            opacity: _fadeAnimation.value,
            child: child,
          ),
        );
      },
      child: widget.child,
    );
  }
}

Performance Tips

1. Optimized Animations

Keep animations at 60fps.

class OptimizedAnimation extends StatefulWidget {
  @override
  _OptimizedAnimationState createState() => _OptimizedAnimationState();
}

class _OptimizedAnimationState extends State<OptimizedAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary( // Isolate repaints
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          // Only rebuild what's necessary
          return Transform.rotate(
            angle: _controller.value * 2 * math.pi,
            child: child, // Pass child to avoid rebuilding
          );
        },
        child: const Icon(Icons.refresh), // const widget
      ),
    );
  }
}

2. Animation Curves

Choose appropriate easing functions.

// Common curves and their use cases
final curves = {
  'ease': Curves.ease, // Gentle acceleration and deceleration
  'easeIn': Curves.easeIn, // Slow start, fast end
  'easeOut': Curves.easeOut, // Fast start, slow end
  'easeInOut': Curves.easeInOut, // Slow start and end, fast middle
  'bounceIn': Curves.bounceIn, // Bouncy entrance
  'elasticOut': Curves.elasticOut, // Elastic exit
  'fastOutSlowIn': Curves.fastOutSlowIn, // Material design
};