跳转到正文
YGJoon 技术博客

Flutter 自定义动画详解

发布于:
·1.4k 字 · 约 3 分钟·作者:
内置 Tween 不够用就自己写:在 lerp 里定义插值规则,把颜色、圆角、阴影打包进一个 BoxDecorationTween,动画组件按需定制
Flutter 自定义动画详解文章封面
目录

简介

Flutter 提供了丰富的动画系统,除了使用内置的动画组件外,我们还可以通过自定义 Tween、动画控制器和动画组件来实现更加复杂的动画效果。

自定义 Tween

基本实现

class ColorTween extends Tween<Color> {
  ColorTween({required Color begin, required Color end})
      : super(begin: begin, end: end);

  @override
  Color lerp(double t) {
    return Color.lerp(begin, end, t)!;
  }
}

// 使用示例
final colorAnimation = ColorTween(
  begin: Colors.blue,
  end: Colors.red,
).animate(_controller);

复杂 Tween

class BoxDecorationTween extends Tween<BoxDecoration> {
  final ColorTween? colorTween;
  final BorderRadiusTween? borderRadiusTween;
  final BoxShadowTween? shadowTween;

  BoxDecorationTween({
    BoxDecoration? begin,
    BoxDecoration? end,
  }) : colorTween = begin?.color != null || end?.color != null
            ? ColorTween(
                begin: begin?.color ?? Colors.transparent,
                end: end?.color ?? Colors.transparent,
              )
            : null,
       borderRadiusTween = begin?.borderRadius != null || 
                          end?.borderRadius != null
            ? BorderRadiusTween(
                begin: begin?.borderRadius as BorderRadius?,
                end: end?.borderRadius as BorderRadius?,
              )
            : null,
       shadowTween = begin?.boxShadow?.first != null || 
                    end?.boxShadow?.first != null
            ? BoxShadowTween(
                begin: begin?.boxShadow?.first,
                end: end?.boxShadow?.first,
              )
            : null,
       super(begin: begin, end: end);

  @override
  BoxDecoration lerp(double t) {
    return BoxDecoration(
      color: colorTween?.lerp(t),
      borderRadius: borderRadiusTween?.lerp(t),
      boxShadow: shadowTween?.lerp(t) != null 
          ? [shadowTween!.lerp(t)!] 
          : null,
    );
  }
}

自定义动画控制器

基本实现

class CustomAnimationController extends AnimationController {
  CustomAnimationController({
    required TickerProvider vsync,
    Duration? duration,
    Duration? reverseDuration,
  }) : super(
          vsync: vsync,
          duration: duration,
          reverseDuration: reverseDuration,
        );

  void playWithDelay(Duration delay) async {
    await Future.delayed(delay);
    forward();
  }

  void repeatWithInterval(Duration interval) async {
    while (true) {
      await forward();
      await Future.delayed(interval);
      await reverse();
      await Future.delayed(interval);
    }
  }
}

使用示例

class CustomAnimationDemo extends StatefulWidget {
  @override
  _CustomAnimationDemoState createState() => _CustomAnimationDemoState();
}

class _CustomAnimationDemoState extends State<CustomAnimationDemo>
    with SingleTickerProviderStateMixin {
  late CustomAnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = CustomAnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );
    _controller.playWithDelay(Duration(seconds: 2));
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _controller.value,
          child: child,
        );
      },
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    );
  }
}

自定义动画组件

基本实现

class CustomAnimatedWidget extends StatefulWidget {
  final Widget child;
  final Duration duration;
  final Curve curve;

  CustomAnimatedWidget({
    required this.child,
    this.duration = const Duration(milliseconds: 300),
    this.curve = Curves.easeInOut,
  });

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

class _CustomAnimatedWidgetState extends State<CustomAnimatedWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: widget.duration,
      vsync: this,
    );
    _animation = CurvedAnimation(
      parent: _controller,
      curve: widget.curve,
    );
    _controller.forward();
  }

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

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

完整示例

class WaveAnimation extends StatefulWidget {
  @override
  _WaveAnimationState createState() => _WaveAnimationState();
}

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

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

    _animations = List.generate(
      5,
      (index) => Tween<double>(
        begin: 0.0,
        end: 1.0,
      ).animate(
        CurvedAnimation(
          parent: _controller,
          curve: Interval(
            index * 0.2,
            (index + 1) * 0.2,
            curve: Curves.easeOutCirc,
          ),
        ),
      ),
    );

    _controller.repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Stack(
            alignment: Alignment.center,
            children: List.generate(5, (index) {
              return Transform.scale(
                scale: 1 + _animations[index].value,
                child: Container(
                  width: 100,
                  height: 100,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: Colors.blue.withOpacity(
                      (1 - _animations[index].value) * 0.5,
                    ),
                  ),
                ),
              );
            }),
          );
        },
      ),
    );
  }
}

最佳实践

  1. 先穷尽内置:Curves 有几十个,Tween 也有 ColorTween/BorderRadiusTween 等十几种——“两个 BoxDecoration 带圆角和阴影一起过渡”官方文档就有现成模式;只有内置表达不了”值”的插值规则时,才值得手写 lerp。
  2. 自定义 Tween 的 lerp 必须保证 t=0 返回 begin、t=1 返回 end;Color.lerp 在两端都为 null 时返回 null——用 ! 断言前先想清楚 null 语义,别无脑断言,否则切主题/透明色过渡时直接 crash。
  3. 写完收进组件库:自定义动画类是长期维护成本,别散落在业务页面——放进 ui 包或组件目录,配好默认 duration 和 curve 参数,业务方只传 child 调用。
  4. 自定义 controller 的 async 方法(如文中的 playWithDelay)在 await 之后先检查 mounted:等待期间页面可能已 pop,此时再 forward 会在已释放的 controller 上抛异常。

注意事项

  1. lerp 每帧都会被调用:里面只做纯数值插值,别分配对象、别调 setState、别读 MediaQuery——高频调用下这些都会变成帧率杀手;需要外部数据先在外面算好传进来。
  2. 继承 AnimationController 加 async 循环(如 repeatWithInterval 的 while(true))必须有退出条件:页面 dispose 时跳出循环,否则 controller dispose 后 forward 抛异常,无限循环还一直占着事件循环。
  3. 自定义 Curve/Tween 配三个单元断言:transform(0)==0、transform(1)==1、lerp(0.5) 落在 begin/end 中点——5 分钟写完,能拦住大部分”我改了一行结果动画抽风”。

实战:这个场景我还没在项目里遇到

说实话,手写自定义 Tween、自定义 Curve 的场景,我在实际项目里还没遇到过——电商 App 的动画需求,隐式动画加 Interval 组合基本全覆盖了,carousel_slider 这类组件又把轮播这种高频需求包掉了。

但我按官方文档的思路给自己定了条线:只有当内置 Tween 表达不了”值”的变化时,才值得写自定义 Tween——比如两个 BoxDecoration 之间带圆角和阴影一起过渡,官方文档的 BoxDecorationTween 示例就是干这个的;自定义 Curve 同理,先看 Curves 内置的几十个够不够,不够再写 transform。自定义动画的代价是维护成本:每个自定义类都是一份长期要读的代码,能不写就不写,真写了就把它收进组件库,别散落在业务页面里。

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

总结

自定义动画为我们提供了更大的灵活性,通过合理使用可以实现更加复杂和独特的动画效果。理解并掌握自定义动画的开发对于创建高质量的 Flutter 应用很有帮助。


分享这篇文章:


相关文章

上一篇
Flutter 路由动画详解
下一篇
Flutter 嵌套路由详解