跳转到正文
YGJoon 技术博客

Flutter 交错动画详解

发布于:
·1.4k 字 · 约 3 分钟·作者:
一个 Controller 驱动多个 Tween:用 Interval 给透明度、宽高、圆角各划一段播放时间,元素依次登场做出层次感
Flutter 交错动画详解文章封面
目录

简介

交错动画是指多个动画按照一定的时间间隔依次播放的动画效果。通过合理设置动画的延迟和间隔,可以创建出富有层次感的动画效果。

基本用法

使用 Interval

class StaggeredAnimationDemo extends StatefulWidget {
  @override
  _StaggeredAnimationDemoState createState() => _StaggeredAnimationDemoState();
}

class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _opacity;
  late Animation<double> _width;
  late Animation<double> _height;
  late Animation<EdgeInsets> _padding;
  late Animation<BorderRadius?> _borderRadius;
  late Animation<Color?> _color;

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

    _opacity = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.2, curve: Curves.ease),
    ));

    _width = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.2, 0.4, curve: Curves.ease),
    ));

    _height = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.4, 0.6, curve: Curves.ease),
    ));

    _padding = EdgeInsetsTween(
      begin: EdgeInsets.only(bottom: 16.0),
      end: EdgeInsets.only(bottom: 75.0),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.6, 0.8, curve: Curves.ease),
    ));

    _borderRadius = BorderRadiusTween(
      begin: BorderRadius.circular(4.0),
      end: BorderRadius.circular(75.0),
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.8, 1.0, curve: Curves.ease),
    ));

    _color = ColorTween(
      begin: Colors.blue,
      end: Colors.purple,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(0.8, 1.0, curve: Curves.ease),
    ));
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Staggered Animation')),
      body: GestureDetector(
        onTap: () {
          if (_controller.status == AnimationStatus.completed) {
            _controller.reverse();
          } else {
            _controller.forward();
          }
        },
        child: Center(
          child: 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,
                      borderRadius: _borderRadius.value,
                    ),
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

列表交错动画

基本实现

class StaggeredListDemo extends StatefulWidget {
  @override
  _StaggeredListDemoState createState() => _StaggeredListDemoState();
}

class _StaggeredListDemoState extends State<StaggeredListDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  final List<String> _items = List.generate(20, (i) => 'Item $i');

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

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

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _items.length,
      itemBuilder: (context, index) {
        return AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            final delay = index * 0.1;
            final animation = CurvedAnimation(
              parent: _controller,
              curve: Interval(
                delay.clamp(0.0, 1.0),
                (delay + 0.1).clamp(0.0, 1.0),
                curve: Curves.easeOut,
              ),
            );

            return SlideTransition(
              position: Tween<Offset>(
                begin: Offset(1.0, 0.0),
                end: Offset.zero,
              ).animate(animation),
              child: FadeTransition(
                opacity: animation,
                child: child,
              ),
            );
          },
          child: ListTile(
            title: Text(_items[index]),
          ),
        );
      },
    );
  }
}

高级用法

组合动画

class CombinedStaggeredAnimation extends StatelessWidget {
  final Animation<double> controller;
  final Animation<double> opacity;
  final Animation<double> dx;
  final Animation<double> dy;
  final Animation<Color?> color;

  CombinedStaggeredAnimation({required this.controller})
      : opacity = Tween<double>(
          begin: 0.0,
          end: 1.0,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.0, 0.3, curve: Curves.ease),
        )),
        dx = Tween<double>(
          begin: -100.0,
          end: 0.0,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.3, 0.6, curve: Curves.ease),
        )),
        dy = Tween<double>(
          begin: -50.0,
          end: 0.0,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.6, 0.9, curve: Curves.ease),
        )),
        color = ColorTween(
          begin: Colors.blue,
          end: Colors.purple,
        ).animate(CurvedAnimation(
          parent: controller,
          curve: Interval(0.9, 1.0, curve: Curves.ease),
        ));

  Widget _buildAnimation(BuildContext context, Widget? child) {
    return Transform.translate(
      offset: Offset(dx.value, dy.value),
      child: Opacity(
        opacity: opacity.value,
        child: Container(
          width: 100,
          height: 100,
          decoration: BoxDecoration(
            color: color.value,
            borderRadius: BorderRadius.circular(10),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      builder: _buildAnimation,
      animation: controller,
    );
  }
}

最佳实践

  1. 总时长控制在 600ms1.2s,分段 Interval 做 60%80% 的重叠才有”波浪”感——首尾相接的纯串行(0.00.2、0.20.4)看起来是一个接一个,节奏感弱;每段宽 0.3、间隔 0.1 这种错开式,元素依次登场才有层次。
  2. 长列表只给首屏 item 做交错入场(比如前 8 个),后面的直接静态渲染,上拉加载更多时不再触发——上百个 item 全做交错,低端机先保证滚动不掉帧再谈韵律感。
  3. 一个 AnimationController 驱动全场,别给每个 item 单独建 controller——20 个 item 就是 20 个 Ticker,每帧 20 次回调,开销和漏 dispose 的风险都翻倍。
  4. per-item 的 Interval/CurvedAnimation 在 initState 里按 index 一次性生成好存进 List,不要在 itemBuilder 里 new——ListView 滚动时 itemBuilder 被反复调用,动画对象每帧重建且无法复用。

注意事项

  1. Interval 的 begin 必须小于 end 且落在 0~1 内;delay 用 index*0.1 累加时,长列表一定要 clamp——index>10 的 item 的 begin 超过 1.0 会直接”跳到终态”,动画悄无声息地不播(本文列表示例里的 clamp 就是干这个的)。
  2. reverse() 倒播时 Interval 顺序也会反转——“依次登场”会变成”依次退场”,如果退场顺序看着别扭,考虑给反向单独配一组 Interval。
  3. 一次性入场动画播完别 repeat——列表每 1s 重播一轮入场,用户滚动时极其干扰;repeat 只留给呼吸灯、加载指示这种本就该循环的场景。
  4. 交错动画的 builder 里只放属性变化的 widget,静态内容走 AnimatedBuilder 的 child 参数——长列表里整张卡片每帧重建是掉帧头号原因。

实战:交错动画别在长列表里滥用

电商首页的商品流、订单列表,item 依次滑入淡出的效果确实好看——这篇里用 Interval 错开时间的写法,和 Flutter 官方文档的 staggered 示例是同一套思路,一个 AnimationController 控住所有 Animation,每个属性一段 Interval。

但我实际做的时候踩过犹豫:上百个 item 的列表,每个 item 都包 AnimatedBuilder + CurvedAnimation,光是上百个 Interval 对象和每帧的重建开销就让人心里没底。我的取舍是:只给首屏前几个 item(比如前 8 个)做交错入场,后面的直接静态渲染;而且只在页面首次进入时播一次,上拉加载更多时不再触发。交错动画是”锦上添花”型——低端机上先保证列表滚动不掉帧,再谈韵律感,这个顺序别反了。

参考:Flutter 官方文档《Staggered animations》(docs.flutter.dev/ui/animations/staggered-animations)

总结

交错动画可以创建出富有层次感和韵律感的动画效果,通过合理使用可以显著提升应用的用户体验。理解并掌握交错动画的使用对于开发高质量的 Flutter 应用很有帮助。


分享这篇文章:


相关文章

上一篇
Flutter Hero 动画详解
下一篇
Flutter 路由动画详解