返回網誌
進階2025年3月15日安安科技團隊閱讀時間 15 分鐘

進階 FLUTTER
動畫

掌握複雜動畫與過場效果,在 Flutter 中打造令人驚艷的用戶體驗。從自訂物理模擬到 Hero 動畫,一應俱全。

進階 Flutter 動畫
// 簡報

動畫是好應用程式與卓越應用程式之間的分野。它提供視覺回饋、引導用戶注意力,並創造讓用戶難忘的愉悅體驗。Flutter 的動畫系統極為強大,但要真正掌握,就必須理解背後更深層的概念。

本指南將帶你越過基礎動畫,進入進階領域。你會學到如何製作自訂物理模擬、複雜過場,以及自然流暢、引人入勝的動畫。這些正是頂尖應用程式用來打造難忘體驗的技巧。

自訂動畫控制器

動畫控制器(AnimationController)是 Flutter 動畫的核心。理解如何建立並協調多個控制器,會為你帶來無限的創作可能。

advanced_animation_widget.dart
class AdvancedAnimationWidget extends StatefulWidget {
  @override
  _AdvancedAnimationWidgetState createState() => _AdvancedAnimationWidgetState();
}

class _AdvancedAnimationWidgetState extends State<AdvancedAnimationWidget>
    with TickerProviderStateMixin {
  late AnimationController _slideController;
  late AnimationController _fadeController;
  late AnimationController _scaleController;
  
  late Animation<Offset> _slideAnimation;
  late Animation<double> _fadeAnimation;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    
    // Create multiple controllers with different durations
    _slideController = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    
    _fadeController = AnimationController(
      duration: const Duration(milliseconds: 600),
      vsync: this,
    );
    
    _scaleController = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: this,
    );
    
    // Create curved animations
    _slideAnimation = Tween<Offset>(
      begin: const Offset(0, 1),
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: _slideController,
      curve: Curves.elasticOut,
    ));
    
    _fadeAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _fadeController,
      curve: Curves.easeInOut,
    ));
    
    _scaleAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _scaleController,
      curve: Curves.bounceOut,
    ));
  }
  
  void _startSequentialAnimation() async {
    // Start animations in sequence with delays
    _slideController.forward();
    await Future.delayed(const Duration(milliseconds: 200));
    _fadeController.forward();
    await Future.delayed(const Duration(milliseconds: 200));
    _scaleController.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: Listenable.merge([_slideController, _fadeController, _scaleController]),
      builder: (context, child) {
        return SlideTransition(
          position: _slideAnimation,
          child: FadeTransition(
            opacity: _fadeAnimation,
            child: ScaleTransition(
              scale: _scaleAnimation,
              child: Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(20),
                ),
                child: const Center(
                  child: Text('Animated!', style: TextStyle(color: Colors.white, fontSize: 24)),
                ),
              ),
            ),
          ),
        );
      },
    );
  }
}

核心概念

  • 使用 TickerProviderStateMixin 管理多個控制器
  • 以延遲執行串連多個動畫
  • 組合不同類型的動畫,構成複雜效果
  • 務必 dispose 控制器,防止記憶體洩漏

物理動畫

物理模擬能營造最自然的動畫手感。Flutter 提供彈簧、重力與摩擦力模擬,讓你的介面反應靈敏、充滿生氣。

spring_animation_widget.dart
class SpringAnimationWidget extends StatefulWidget {
  @override
  _SpringAnimationWidgetState createState() => _SpringAnimationWidgetState();
}

class _SpringAnimationWidgetState extends State<SpringAnimationWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    
    // Create a spring simulation
    final spring = SpringDescription(
      mass: 1.0,        // Mass of the object
      stiffness: 500.0, // Spring stiffness
      damping: 20.0,    // Damping coefficient
    );
    
    final springSimulation = SpringSimulation(spring, 0.0, 1.0, 0.0);
    
    _animation = _controller.drive(
      CurveTween(curve: Curve.from(springSimulation)),
    );
  }
  
  void _startSpringAnimation() {
    _controller.reset();
    _controller.forward();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Transform.translate(
          offset: Offset(0, -100 * _animation.value),
          child: Container(
            width: 100,
            height: 100,
            decoration: const BoxDecoration(
              color: Colors.red,
              shape: BoxShape.circle,
            ),
          ),
        );
      },
    );
  }
}

// Custom physics simulation
class BouncingBallSimulation extends Simulation {
  final double gravity;
  final double bounciness;
  final double initialVelocity;
  
  BouncingBallSimulation({
    required this.gravity,
    required this.bounciness,
    required this.initialVelocity,
  });
  
  @override
  double x(double time) {
    // Calculate position based on physics
    return initialVelocity * time + 0.5 * gravity * time * time;
  }
  
  @override
  double dx(double time) {
    // Calculate velocity
    return initialVelocity + gravity * time;
  }
  
  @override
  bool isDone(double time) {
    return false; // Continue indefinitely
  }
}

物理模擬貼士

  • 阻尼(damping)越低越有彈性,越高則越受控
  • 調整質量(mass)可改變動畫的「重量感」
  • 使用重力模擬營造逼真的下墜效果
  • 疊加多種物理模擬,構成複雜行為

HERO 動畫與過場

Hero 動畫能在頁面之間建立無縫過場。掌握自訂 Hero 動畫,打造獨特的導航體驗,引導用戶穿梭於應用程式的流程之中。

custom_hero_animation.dart
// Custom Hero Animation
class CustomHeroAnimation extends StatelessWidget {
  final String heroTag;
  final Widget child;
  final Duration duration;
  
  const CustomHeroAnimation({
    required this.heroTag,
    required this.child,
    this.duration = const Duration(milliseconds: 500),
  });
  
  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: heroTag,
      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: Transform.rotate(
                angle: animation.value * 2 * math.pi,
                child: Material(
                  type: MaterialType.transparency,
                  child: toHeroContext.widget,
                ),
              ),
            );
          },
        );
      },
      child: child,
    );
  }
}

// Advanced Page Transition
class SlideUpPageRoute<T> extends PageRoute<T> {
  final Widget child;
  final Duration duration;
  
  SlideUpPageRoute({
    required this.child,
    this.duration = const Duration(milliseconds: 300),
  });
  
  @override
  Color? get barrierColor => null;
  
  @override
  String? get barrierLabel => null;
  
  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return child;
  }
  
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    final slideAnimation = Tween<Offset>(
      begin: const Offset(0, 1),
      end: Offset.zero,
    ).animate(CurvedAnimation(
      parent: animation,
      curve: Curves.easeOutCubic,
    ));
    
    final fadeAnimation = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: animation,
      curve: const Interval(0.3, 1.0),
    ));
    
    return SlideTransition(
      position: slideAnimation,
      child: FadeTransition(
        opacity: fadeAnimation,
        child: child,
      ),
    );
  }
  
  @override
  Duration get transitionDuration => duration;
  
  @override
  bool get maintainState => true;
}

HERO 最佳實踐

  • 使用唯一的 hero tag,避免衝突
  • 自訂 flightShuttleBuilder,做出獨一無二的過場
  • 留意複雜 Hero 動畫對效能的影響
  • 在不同螢幕尺寸上測試 Hero 動畫

CUSTOM PAINT 動畫

CustomPainter 讓你創作任何想像得到的動畫。從形狀漸變到粒子系統,自訂繪製賦予你對動畫像素級的掌控。

animated_wave_painter.dart
class AnimatedWavePainter extends CustomPainter {
  final double animationValue;
  final Color waveColor;
  
  AnimatedWavePainter({
    required this.animationValue,
    required this.waveColor,
  });
  
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = waveColor
      ..style = PaintingStyle.fill;
    
    final path = Path();
    final waveHeight = 20.0;
    final waveLength = size.width / 2;
    
    path.moveTo(0, size.height / 2);
    
    for (double x = 0; x <= size.width; x += 1) {
      final y = size.height / 2 + 
          waveHeight * math.sin((x / waveLength) * 2 * math.pi + animationValue * 2 * math.pi);
      path.lineTo(x, y);
    }
    
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);
    path.close();
    
    canvas.drawPath(path, paint);
  }
  
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return oldDelegate is AnimatedWavePainter && 
           oldDelegate.animationValue != animationValue;
  }
}

class WaveAnimationWidget extends StatefulWidget {
  @override
  _WaveAnimationWidgetState createState() => _WaveAnimationWidgetState();
}

class _WaveAnimationWidgetState extends State<WaveAnimationWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,
    )..repeat();
  }
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return CustomPaint(
          painter: AnimatedWavePainter(
            animationValue: _controller.value,
            waveColor: Colors.blue,
          ),
          size: const Size(300, 200),
        );
      },
    );
  }
}

CUSTOM PAINT 貼士

  • 優化 shouldRepaint 以提升效能
  • 使用 Path.combine 處理複雜形狀運算
  • 把昂貴的計算快取在 paint 方法之外
  • 進階特效可考慮使用 shader

動畫效能秘訣

應該這樣做

  • 為子 widget 使用 const 建構函式
  • 以 RepaintBoundary 優化重繪範圍
  • 使用 AnimatedBuilder 精準重建
  • 妥善 dispose 控制器
  • 使用 Transform,避免改動佈局

避免這樣做

  • 直接對佈局屬性做動畫
  • 在 build 方法內建立控制器
  • 在 paint 方法內進行複雜計算
  • 同時執行過多動畫
  • 忘記釋放(dispose)資源
// 動態

準備好讓你的應用程式動起來?

我們的團隊擅長打造驚艷、高效能又令用戶愉悅的動畫。讓我們以進階 Flutter 動畫,為你的應用程式注入生命!

聯絡我們
// 訊號放大

分享本指南