Chuyển đến nội dung chính

Bài 13: Animations & Motion

Implicit animations, AnimatedContainer, AnimatedOpacity. Explicit animations, AnimationController, Tween, CurvedAnimation. Hero transitions, Staggered animations, Rive/Lottie.

💻 Lập trình — Bài 13 Bài 13: Animations & Motion

Flutter & Dart: Từ Cơ bản đến Nâng cao

Phần 4: Advanced UI & Features

xdev.asia

1. Implicit Animations

class AnimatedCard extends StatefulWidget {
  @override
  State<AnimatedCard> createState() => _AnimatedCardState();
}

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _expanded = !_expanded),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        width: _expanded ? 300 : 200,
        height: _expanded ? 200 : 100,
        decoration: BoxDecoration(
          color: _expanded ? Colors.indigo : Colors.grey[300],
          borderRadius: BorderRadius.circular(_expanded ? 24 : 12),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(_expanded ? 0.3 : 0.1),
              blurRadius: _expanded ? 16 : 4,
              offset: Offset(0, _expanded ? 8 : 2),
            ),
          ],
        ),
        child: AnimatedOpacity(
          opacity: _expanded ? 1.0 : 0.6,
          duration: const Duration(milliseconds: 300),
          child: Center(
            child: AnimatedDefaultTextStyle(
              style: TextStyle(
                fontSize: _expanded ? 24 : 16,
                color: _expanded ? Colors.white : Colors.black,
              ),
              duration: const Duration(milliseconds: 300),
              child: const Text('Tap me'),
            ),
          ),
        ),
      ),
    );
  }
}

2. Explicit Animations

class PulseAnimation extends StatefulWidget {
  @override
  State<PulseAnimation> createState() => _PulseAnimationState();
}

class _PulseAnimationState extends State<PulseAnimation>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;
  late final Animation<double> _scaleAnimation;
  late final Animation<double> _opacityAnimation;

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

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

    _opacityAnimation = Tween<double>(begin: 0.5, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeIn),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Opacity(
          opacity: _opacityAnimation.value,
          child: Transform.scale(
            scale: _scaleAnimation.value,
            child: child,
          ),
        );
      },
      child: const Icon(Icons.favorite, size: 80, color: Colors.red),
    );
  }
}

3. Hero & Page Transitions

// Source page
Hero(
  tag: 'product-${product.id}',
  child: Image.network(product.imageUrl, width: 120, height: 120, fit: BoxFit.cover),
);

// Detail page
Hero(
  tag: 'product-${product.id}',
  child: Image.network(product.imageUrl, width: double.infinity, height: 300, fit: BoxFit.cover),
);

// Custom page transition
GoRoute(
  path: '/detail/:id',
  pageBuilder: (context, state) => CustomTransitionPage(
    child: DetailPage(id: state.pathParameters['id']!),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: CurvedAnimation(parent: animation, curve: Curves.easeIn),
        child: SlideTransition(
          position: Tween<Offset>(begin: const Offset(0, 0.1), end: Offset.zero)
              .animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)),
          child: child,
        ),
      );
    },
  ),
);

4. Lottie Animations

// pubspec.yaml: lottie: ^3.1.0
Lottie.asset(
  'assets/animations/loading.json',
  width: 200,
  height: 200,
  repeat: true,
  animate: true,
);

// Controller-based
class LottieExample extends StatefulWidget {
  @override
  State<LottieExample> createState() => _LottieExampleState();
}

class _LottieExampleState extends State<LottieExample>
    with SingleTickerProviderStateMixin {
  late final AnimationController _controller;

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        _controller.forward(from: 0);
      },
      child: Lottie.asset(
        'assets/animations/like.json',
        controller: _controller,
        onLoaded: (composition) {
          _controller.duration = composition.duration;
        },
      ),
    );
  }
}

Bài tiếp theo: Custom Painting & Advanced Widgets.