返回博客
高级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 动画,为你的应用注入生命!

联系我们
// 信号放大

分享本指南