flutter学习第 14 节:动画与过渡效果

动画是提升用户体验的关键因素,能够使应用界面更加生动、直观,增强用户交互感。Flutter 提供了强大的动画系统,支持各种复杂的动画效果实现。本节课将详细介绍 Flutter 中的动画类型和实现方式,从基础的隐式动画到复杂的自定义显式动画,帮助你掌握为应用添加流畅动画效果的技能。

一、Flutter 动画基础

Flutter 动画系统基于以下核心概念:

  • Animation:一个生成介于 0.0 和 1.0 之间数值的对象,它本身不包含渲染内容,只提供动画数值
  • Curve:定义动画进度的速度变化,如加速、减速等
  • Controller:控制动画的播放、暂停、反向等,管理动画生命周期
  • Tween:定义动画的起始值和结束值,将 Animation 提供的 0-1 数值映射到实际需要的数值范围
  • Animatable:可以生成 Tween 的对象,支持更复杂的数值映射

Flutter 动画主要分为两类:

  • 隐式动画:由 Flutter 自动管理的动画,只需定义起始和结束状态
  • 显式动画:需要手动控制的动画,提供更多自定义选项和控制能力

二、隐式动画

隐式动画(Implicit Animations)是最简单的动画实现方式,Flutter 提供了一系列封装好的隐式动画组件,当组件的某些属性发生变化时,会自动从旧值平滑过渡到新值。

1. AnimatedContainer

AnimatedContainer 是最常用的隐式动画组件,当它的属性(如大小、颜色、边距等)发生变化时,会自动产生过渡动画。

dart 复制代码
class AnimatedContainerDemo extends StatefulWidget {
  const AnimatedContainerDemo({super.key});

  @override
  State<AnimatedContainerDemo> createState() => _AnimatedContainerDemoState();
}

class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
  // 控制容器属性的状态变量
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedContainer Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedContainer(
              // 动画持续时间
              duration: const Duration(seconds: 1),
              // 动画曲线
              curve: Curves.easeInOut,
              // 动态变化的属性
              width: _isExpanded ? 300 : 100,
              height: _isExpanded ? 300 : 100,
              color: _isExpanded ? Colors.blue : Colors.red,
              padding: _isExpanded
                  ? const EdgeInsets.all(20)
                  : const EdgeInsets.all(10),
              // 容器内的内容
              child: const Center(
                child: Text(
                  'Animate me!',
                  style: TextStyle(color: Colors.white, fontSize: 20),
                ),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 点击按钮切换状态,触发动画
                setState(() {
                  _isExpanded = !_isExpanded;
                });
              },
              child: const Text('Toggle Animation'),
            ),
          ],
        ),
      ),
    );
  }
}

2. 其他常用隐式动画组件

Flutter 还提供了其他专门用途的隐式动画组件:

AnimatedOpacity

控制组件透明度变化的动画:

dart 复制代码
class AnimatedOpacityDemo extends StatefulWidget {
  const AnimatedOpacityDemo({super.key});

  @override
  State<AnimatedOpacityDemo> createState() => _AnimatedOpacityDemoState();
}

class _AnimatedOpacityDemoState extends State<AnimatedOpacityDemo> {
  double _opacity = 1.0;

  void _toggleOpacity() {
    setState(() {
      _opacity = _opacity == 1.0 ? 0.0 : 1.0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedOpacity Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AnimatedOpacity(
              opacity: _opacity,
              duration: const Duration(seconds: 1),
              curve: Curves.fastOutSlowIn,
              child: Container(
                width: 200,
                height: 200,
                color: Colors.green,
                child: const Center(
                  child: Text(
                    'Fade me!',
                    style: TextStyle(color: Colors.white, fontSize: 24),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _toggleOpacity,
              child: const Text('Toggle Opacity'),
            ),
          ],
        ),
      ),
    );
  }
}

AnimatedPositioned

用于 Stack 中,控制子组件位置变化的动画:

dart 复制代码
class AnimatedPositionedDemo extends StatefulWidget {
  const AnimatedPositionedDemo({super.key});

  @override
  State<AnimatedPositionedDemo> createState() => _AnimatedPositionedDemoState();
}

class _AnimatedPositionedDemoState extends State<AnimatedPositionedDemo> {
  bool _isMoved = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedPositioned Demo')),
      body: Stack(
        children: [
          AnimatedPositioned(
            duration: const Duration(seconds: 1),
            curve: Curves.bounceOut,
            left: _isMoved ? 200 : 50,
            top: _isMoved ? 300 : 100,
            width: 100,
            height: 100,
            child: Container(
              color: Colors.purple,
              child: const Center(
                child: Text(
                  'Move me!',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
          Positioned(
            bottom: 50,
            left: 0,
            right: 0,
            child: Center(
              child: ElevatedButton(
                onPressed: () {
                  setState(() {
                    _isMoved = !_isMoved;
                  });
                },
                child: const Text('Move the Box'),
              ),
            ),
          )
        ],
      ),
    );
  }
}

AnimatedPadding、AnimatedSize 等

还有其他类似的隐式动画组件,使用方式大同小异:

  • AnimatedPadding:控制内边距变化
  • AnimatedSize:根据子组件大小自动调整并产生动画
  • AnimatedTransform:控制变换效果的动画
  • AnimatedDefaultTextStyle:控制文本样式变化的动画

3. 隐式动画的优缺点

优点

  • 使用简单,只需修改状态即可触发动画
  • 无需手动管理动画控制器
  • 适合实现简单的过渡效果

缺点

  • 定制化程度低,无法实现复杂动画
  • 缺乏精细的控制能力(如暂停、反向播放)
  • 多个属性同时动画时难以协调

三、显式动画

显式动画(Explicit Animations)需要手动创建和管理动画控制器,提供了更精细的控制和更大的灵活性,适合实现复杂的动画效果。

1. AnimationController 与 Animation

AnimationController 是显式动画的核心,负责控制动画的时间和状态:

dart 复制代码
class BasicExplicitAnimation extends StatefulWidget {
  const BasicExplicitAnimation({super.key});

  @override
  State<BasicExplicitAnimation> createState() => _BasicExplicitAnimationState();
}

class _BasicExplicitAnimationState extends State<BasicExplicitAnimation>
    with SingleTickerProviderStateMixin {
  // 动画控制器
  late AnimationController _controller;
  // 动画对象
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    // 初始化动画控制器
    _controller = AnimationController(
      vsync: this, // 与当前组件生命周期关联
      duration: const Duration(seconds: 2), // 动画持续时间
    );

    // 创建从 0 到 300 的动画
    _animation = Tween<double>(begin: 0, end: 300).animate(_controller)
      // 监听动画值变化,触发重建
      ..addListener(() {
        setState(() {});
      })
      // 监听动画状态变化
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          // 动画完成后反向播放
          _controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          // 动画回到起点后正向播放
          _controller.forward();
        }
      });

    // 开始动画
    _controller.forward();
  }

  @override
  void dispose() {
    // 释放动画控制器资源
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Basic Explicit Animation')),
      body: Center(
        child: Container(
          width: _animation.value, // 使用动画值
          height: _animation.value,
          color: Colors.orange,
          child: const Center(
            child: Text(
              'Growing Box',
              style: TextStyle(color: Colors.white, fontSize: 18),
            ),
          ),
        ),
      ),
    );
  }
}

注意:SingleTickerProviderStateMixin 提供了一个 vsync 回调,用于防止动画在组件不可见时继续消耗资源。如果需要多个动画控制器,应使用 TickerProviderStateMixin

2. CurvedAnimation 曲线动画

使用 CurvedAnimation 可以为动画添加非线性的速度变化:

dart 复制代码
class CurvedAnimationDemo extends StatefulWidget {
  const CurvedAnimationDemo({super.key});

  @override
  State<CurvedAnimationDemo> createState() => _CurvedAnimationDemoState();
}

class _CurvedAnimationDemoState extends State<CurvedAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );

    // 创建曲线动画
    final curve = CurvedAnimation(
      parent: _controller,
      curve: Curves.bounceOut, // 弹跳效果
      reverseCurve: Curves.bounceIn, // 反向时的曲线
    );

    // 使用曲线动画
    _animation = Tween<double>(begin: 50, end: 300).animate(curve)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          _controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          _controller.forward();
        }
      });

    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Curved Animation')),
      body: Center(
        child: Container(
          width: _animation.value,
          height: 100,
          color: Colors.pink,
          child: const Center(
            child: Text(
              'Bouncy!',
              style: TextStyle(color: Colors.white, fontSize: 20),
            ),
          ),
        ),
      ),
    );
  }
}

Flutter 提供了多种预定义的曲线,如:

  • Curves.linear:线性动画
  • Curves.easeInCurves.easeOutCurves.easeInOut:缓入、缓出、缓入缓出
  • Curves.bounceInCurves.bounceOut:弹跳效果
  • Curves.elasticInCurves.elasticOut:弹性效果

3. Tween 与多属性动画

Tween 定义了动画的取值范围,可以是任意类型。多个 Tween 可以组合实现多属性动画:

dart 复制代码
class MultiPropertyAnimation extends StatefulWidget {
  const MultiPropertyAnimation({super.key});

  @override
  State<MultiPropertyAnimation> createState() => _MultiPropertyAnimationState();
}

class _MultiPropertyAnimationState extends State<MultiPropertyAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<double> _opacityAnimation;
  late Animation<Color?> _colorAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 3),
    );

    // 大小动画
    _sizeAnimation = Tween<double>(
      begin: 50,
      end: 250,
    ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));

    // 透明度动画
    _opacityAnimation = Tween<double>(begin: 0.2, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.3, 1.0),
      ), // 延迟开始
    );

    // 颜色动画
    _colorAnimation = ColorTween(begin: Colors.blue, end: Colors.green).animate(
      CurvedAnimation(
        parent: _controller,
        curve: const Interval(0.0, 0.7),
      ), // 提前结束
    );

    // 监听动画值变化
    _controller.addListener(() {
      setState(() {});
    });

    // 循环播放
    _controller.repeat(reverse: true);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Multi-property Animation')),
      body: Center(
        child: Opacity(
          opacity: _opacityAnimation.value,
          child: Container(
            width: _sizeAnimation.value,
            height: _sizeAnimation.value,
            decoration: BoxDecoration(
              color: _colorAnimation.value,
              // 使用 borderRadius 属性(已修复)
              borderRadius: BorderRadius.circular(_sizeAnimation.value / 10),
            ),
            child: const Center(
              child: Text(
                'Fancy!',
                style: TextStyle(color: Colors.white, fontSize: 24),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Interval 可以为不同的动画属性设置不同的时间区间,实现更复杂的动画编排。

4. AnimatedBuilder 优化重建性能

使用 addListener 配合 setState 会导致整个组件重建,使用 AnimatedBuilder 可以只重建需要动画的部分,提高性能:

dart 复制代码
class AnimatedBuilderDemo extends StatefulWidget {
  const AnimatedBuilderDemo({super.key});

  @override
  State<AnimatedBuilderDemo> createState() => _AnimatedBuilderDemoState();
}

class _AnimatedBuilderDemoState extends State<AnimatedBuilderDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _rotationAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 5),
    );

    _rotationAnimation = Tween<double>(
      begin: 0,
      end: 2 * 3.14159,
    ).animate(CurvedAnimation(parent: _controller, curve: Curves.linear));

    _controller.repeat();
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('AnimatedBuilder Demo')),
      body: Center(
        // 使用 AnimatedBuilder 包裹需要动画的部分
        child: AnimatedBuilder(
          animation: _rotationAnimation,
          // builder 方法只重建动画相关部分
          builder: (context, child) {
            return Transform.rotate(
              angle: _rotationAnimation.value,
              child: child, // 传入静态子组件,避免重复构建
            );
          },
          // 静态子组件,只会构建一次
          child: Container(
            width: 200,
            height: 200,
            decoration: BoxDecoration(
              color: Colors.amber,
              // 使用 borderRadius 属性(已修复)
              borderRadius: BorderRadius.circular(20),
            ),
            child: const Center(
              child: Text(
                'Spinning!',
                style: TextStyle(color: Colors.black, fontSize: 24),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

四、页面过渡动画

Flutter 允许自定义页面之间的切换动画,通过 PageRouteBuilder 可以实现各种过渡效果。

1. 淡入淡出过渡

dart 复制代码
class FadeTransitionDemo extends StatelessWidget {
  const FadeTransitionDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Fade Transition Demo')),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go to Second Screen'),
          onPressed: () {
            // 导航到第二个页面,使用自定义过渡动画
            Navigator.push(
              context,
              PageRouteBuilder(
                transitionDuration: const Duration(seconds: 1), // 过渡时间
                // 构建页面内容
                pageBuilder: (context, animation, secondaryAnimation) {
                  return const SecondScreen();
                },
                // 构建过渡效果
                transitionsBuilder:
                    (context, animation, secondaryAnimation, child) {
                      // 淡入淡出过渡
                      return FadeTransition(opacity: animation, child: child);
                    },
              ),
            );
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
        backgroundColor: Colors.green,
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text('Go Back'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

2. 滑动过渡

dart 复制代码
// 在导航时使用这个过渡
PageRouteBuilder(
  transitionDuration: const Duration(milliseconds: 500),
  pageBuilder: (context, animation, secondaryAnimation) {
    return const SecondScreen();
  },
  transitionsBuilder: (context, animation, secondaryAnimation, child) {
    // 滑动过渡 - 从右侧滑入
    const begin = Offset(1.0, 0.0); // 起始位置(右侧)
    const end = Offset.zero; // 结束位置(屏幕内)
    const curve = Curves.easeInOut;
    
    // 创建从 begin 到 end 的动画
    var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
    var offsetAnimation = animation.drive(tween);
    
    return SlideTransition(
      position: offsetAnimation,
      child: child,
    );
  },
)

3. 缩放过渡

dart 复制代码
PageRouteBuilder(
  transitionDuration: const Duration(milliseconds: 500),
  pageBuilder: (context, animation, secondaryAnimation) {
    return const SecondScreen();
  },
  transitionsBuilder: (context, animation, secondaryAnimation, child) {
    // 缩放过渡
    return ScaleTransition(
      scale: animation.drive(
        Tween(begin: 0.0, end: 1.0).chain(
          CurveTween(curve: Curves.bounceOut)
        )
      ),
      child: child,
    );
  },
)

4. 组合过渡效果

可以将多种过渡效果组合使用:

dart 复制代码
PageRouteBuilder(
  transitionDuration: const Duration(milliseconds: 700),
  pageBuilder: (context, animation, secondaryAnimation) {
    return const SecondScreen();
  },
  transitionsBuilder: (context, animation, secondaryAnimation, child) {
    // 组合缩放和淡入效果
    return ScaleTransition(
      scale: animation.drive(
        Tween(begin: 0.8, end: 1.0).chain(
          CurveTween(curve: Curves.easeInOut)
        )
      ),
      child: FadeTransition(
        opacity: animation.drive(
          Tween(begin: 0.0, end: 1.0).chain(
            CurveTween(curve: const Interval(0.2, 1.0))
          )
        ),
        child: child,
      ),
    );
  },
)

五、实例:按钮点击缩放效果

实现一个带有点击反馈的按钮,点击时会产生缩放效果:

dart 复制代码
class BounceButton extends StatefulWidget {
  final Widget child;
  final VoidCallback onPressed;
  final double scaleFactor;
  final Duration duration;

  const BounceButton({
    super.key,
    required this.child,
    required this.onPressed,
    this.scaleFactor = 0.8,
    this.duration = const Duration(milliseconds: 300),
  });

  @override
  State<BounceButton> createState() => _BounceButtonState();
}

class _BounceButtonState extends State<BounceButton>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(vsync: this, duration: widget.duration);

    _scaleAnimation = Tween<double>(
      begin: 1.0,
      end: widget.scaleFactor,
    ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
  }

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

  // 处理按下 - 不直接执行forward,由onTapUp统一控制
  void _onTapDown(TapDownDetails details) {
    print("按下:准备启动动画");
  }

  // 处理释放 - 统一控制动画流程,确保完整执行
  void _onTapUp(TapUpDetails details) {
    print("释放:开始动画流程");
    // 先执行完整的正向动画,再执行反向动画,最后触发回调
    _controller.forward().then((_) {
      _controller.reverse().then((_) {
        widget.onPressed();
      });
    });
  }

  // 处理取消
  void _onTapCancel() {
    print("取消:反向动画");
    _controller.reverse();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _scaleAnimation,
      builder: (context, child) {
        return Transform.scale(scale: _scaleAnimation.value, child: child);
      },
      child: GestureDetector(
        onTapDown: _onTapDown,
        onTapUp: _onTapUp,
        onTapCancel: _onTapCancel,
        behavior: HitTestBehavior.opaque,
        child: widget.child,
      ),
    );
  }
}

// 使用示例
class BounceButtonDemo extends StatelessWidget {
  const BounceButtonDemo({super.key});

  void _handlePress() {
    print('Button pressed!');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Bounce Button Demo')),
      body: Center(
        child: BounceButton(
          onPressed: _handlePress,
          scaleFactor: 0.7,
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
            decoration: BoxDecoration(
              color: Colors.blue,
              borderRadius: BorderRadius.circular(12),
              boxShadow: const [
                BoxShadow(
                  color: Colors.black26,
                  blurRadius: 10,
                  offset: Offset(0, 4),
                ),
              ],
            ),
            child: const Text(
              'Click Me',
              style: TextStyle(
                color: Colors.white,
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
      ),
    );
  }
}

说明

  • _onTapDown 中不直接执行 _controller.forward(),只标记按钮被按下状态
  • _onTapUp 中统一控制动画流程,先执行完整的正向动画(按钮缩小),再执行反向动画(按钮恢复),最后才触发业务回调

六、动画性能优化

动画性能对用户体验至关重要,以下是一些优化建议:

  1. 使用 AnimatedBuilder 减少重建范围:只重建需要动画的部分,避免整个页面重建。
  2. 避免在动画过程中执行复杂计算:动画回调中应尽量简洁,复杂计算会导致卡顿。
  3. 使用硬件加速 :大多数动画会自动使用硬件加速,但应避免使用会触发软件渲染的操作(如使用 saveLayer)。
  4. 控制动画帧率:大多数情况下,60fps 已足够,更高的帧率会消耗更多资源。
  5. 合理设置动画时长:一般动画时长在 200-300ms 之间,过长会让用户感到延迟。
  6. 使用 RepaintBoundary 隔离重绘区域 :对于频繁重绘的动画组件,使用 RepaintBoundary 包裹,避免影响其他组件。
dart 复制代码
RepaintBoundary(
  child: AnimatedWidget(...),
)
  1. 避免透明度动画与阴影同时使用:这两种效果同时使用会降低性能,可以在动画期间暂时移除阴影。
相关推荐
安卓机器1 小时前
安卓10.0系统修改定制化____系列 ROM解打包 修改 讲解 导读篇
android·安卓10系统修改
小仙女喂得猪2 小时前
2025再读Android RecyclerView源码
android·android studio
BoomHe2 小时前
车载 XCU 的简单介绍
android
锅拌饭2 小时前
RecyclerView 缓存复用导致动画失效问题
android
程序员老刘3 小时前
操作系统“卡脖子”到底是个啥?
android·开源·操作系统
拭心3 小时前
一键生成 Android 适配不同分辨率尺寸的图片
android·开发语言·javascript
2501_915918414 小时前
iOS 文件管理全流程实战,从开发调试到数据迁移
android·ios·小程序·https·uni-app·iphone·webview
songgeb4 小时前
UIScene in iOS
ios·ui kit
想想肿子会怎么做4 小时前
Flutter 环境安装
前端·flutter