欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

动画是 Flutter 应用的 “灵魂”—— 一个流畅的动画能让按钮点击、页面切换、数据加载等交互瞬间变得生动,而糟糕的动画则会让应用显得卡顿、廉价。但很多开发者仅停留在使用系统自带动画的层面,遇到自定义动画、复杂交互动画时就束手无策。本文将从基础的显隐动画入手,逐步实现 Hero 转场动画、物理动画、自定义动画控制器等进阶功能,既有严谨的代码规范,又有生动的场景拆解,让你彻底掌握 Flutter 动画开发的精髓。

一、Flutter 动画核心认知:为什么动画开发容易踩坑?

先理清 Flutter 动画的底层逻辑,避开新手常见误区:

  • 动画的核心是 “状态插值 + 帧刷新”:Flutter 动画通过Animation对象管理插值(从初始值到目标值的渐变),AnimationController控制动画生命周期,AnimatedWidget/AnimatedBuilder实现 UI 刷新;
  • 动画分类
    • 隐式动画:系统封装的动画(如AnimatedOpacity/AnimatedContainer),只需修改状态,自动生成动画;
    • 显式动画:手动控制动画控制器(如AnimationController+CurvedAnimation),灵活性更高;
    • 转场动画:页面 / 组件切换动画(如Hero/PageRouteBuilder);
    • 物理动画:模拟真实物理规律的动画(如SpringSimulation/GravitySimulation);
  • 性能优化点:避免动画过程中重建 Widget、使用RepaintBoundary隔离重绘区域、合理设置动画曲线。

本文所有代码基于:

plaintext

Flutter 3.32.0
Dart 3.9.0

二、入门:隐式动画(一键实现基础动效)

隐式动画是 Flutter 最友好的动画方式,无需手动管理控制器,只需修改 Widget 的属性,系统会自动生成过渡动画。我们先实现 “按钮点击显隐 + 容器尺寸 / 颜色渐变” 的基础案例。

2.1 完整代码实现

dart

import 'package:flutter/material.dart';

void main() => runApp(const AnimationDemoApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter动画实战',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const ImplicitAnimationPage(),
    );
  }
}

class ImplicitAnimationPage extends StatefulWidget {
  const ImplicitAnimationPage({super.key});

  @override
  State<ImplicitAnimationPage> createState() => _ImplicitAnimationPageState();
}

class _ImplicitAnimationPageState extends State<ImplicitAnimationPage> {
  // 控制容器显隐
  bool _isVisible = true;
  // 控制容器尺寸
  double _containerSize = 200;
  // 控制容器颜色
  Color _containerColor = Colors.blue;
  // 控制容器圆角
  double _borderRadius = 16;

  // 切换显隐状态
  void _toggleVisibility() {
    setState(() {
      _isVisible = !_isVisible;
    });
  }

  // 随机修改容器属性
  void _randomizeContainer() {
    setState(() {
      _containerSize = 100 + (Math.random() * 200); // 100-300px
      _containerColor = Color.fromRGBO(
        Random().nextInt(256),
        Random().nextInt(256),
        Random().nextInt(256),
        1.0,
      );
      _borderRadius = Random().nextDouble() * 50; // 0-50px
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('隐式动画实战')),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            // 核心:AnimatedOpacity实现显隐动画
            AnimatedOpacity(
              // 透明度:显示1.0,隐藏0.0
              opacity: _isVisible ? 1.0 : 0.0,
              // 动画时长
              duration: const Duration(milliseconds: 500),
              // 动画曲线(缓入缓出)
              curve: Curves.easeInOut,
              // 动画结束后的状态(隐藏时不占空间)
              child: AnimatedContainer(
                // 尺寸动画
                width: _containerSize,
                height: _containerSize,
                // 颜色动画
                color: _containerColor,
                // 圆角动画
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(_borderRadius),
                ),
                // 动画时长(可单独设置)
                duration: const Duration(milliseconds: 800),
                // 不同属性可设置不同曲线
                curve: Curves.bounceOut,
                // 容器内的内容
                child: const Center(
                  child: Text(
                    '隐式动画',
                    style: TextStyle(color: Colors.white, fontSize: 24),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 40),
            // 操作按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _toggleVisibility,
                  child: Text(_isVisible ? '隐藏容器' : '显示容器'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _randomizeContainer,
                  child: const Text('随机修改容器'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

2.2 核心代码解析

1. 隐式动画核心控件
  • AnimatedOpacity:专门用于透明度动画的隐式控件,核心属性:
    • opacity:目标透明度(0.0-1.0);
    • duration:动画时长;
    • curve:动画曲线(控制动画的速度变化);
  • AnimatedContainer:万能的隐式动画控件,支持尺寸、颜色、圆角、边距等几乎所有容器属性的动画,只需修改属性值,自动生成过渡动画;
  • 其他常用隐式动画
    • AnimatedSize:尺寸动画;
    • AnimatedPositioned:定位动画(需配合Stack);
    • AnimatedSwitcher:组件切换动画;
    • AnimatedTextStyle:文本样式动画。
2. 动画曲线(Curves)

Flutter 内置了丰富的动画曲线,不同曲线对应不同的动画效果:

  • Curves.easeInOut:缓入缓出(默认,最自然);
  • Curves.bounceOut:回弹效果(类似皮球落地);
  • Curves.elasticOut:弹性效果;
  • Curves.decelerate:减速效果;
  • Curves.linear:线性匀速效果。
3. 关键优化点
  • 状态驱动动画:所有动画都由setState修改状态触发,无需手动控制动画生命周期;
  • 动画叠加AnimatedOpacity嵌套AnimatedContainer实现多属性同时动画,Flutter 会自动处理动画叠加;
  • 性能优势:隐式动画内部已优化重绘逻辑,只会更新变化的属性,不会重建整个 Widget。

三、进阶:显式动画(手动控制动画生命周期)

隐式动画虽然简单,但灵活性不足(无法控制动画的暂停、反转、重复等)。显式动画通过AnimationController手动控制动画,适合复杂的交互场景。

3.1 实现自定义显式动画(进度条 + 旋转动画)

dart

import 'package:flutter/material.dart';

class ExplicitAnimationPage extends StatefulWidget {
  const ExplicitAnimationPage({super.key});

  @override
  State<ExplicitAnimationPage> createState() => _ExplicitAnimationPageState();
}

class _ExplicitAnimationPageState extends State<ExplicitAnimationPage>
    with SingleTickerProviderStateMixin {
  // 动画控制器(核心)
  late AnimationController _controller;
  // 进度动画(0.0-1.0)
  late Animation<double> _progressAnimation;
  // 旋转动画(0-2π)
  late Animation<double> _rotationAnimation;
  // 颜色动画(蓝色→红色→绿色)
  late Animation<Color?> _colorAnimation;

  @override
  void initState() {
    super.initState();
    // 初始化动画控制器
    _controller = AnimationController(
      // 动画时长
      duration: const Duration(seconds: 2),
      // vsync:防止动画在后台运行(需混入SingleTickerProviderStateMixin)
      vsync: this,
      // 动画范围(0.0-1.0)
      lowerBound: 0.0,
      upperBound: 1.0,
    );

    // 1. 进度动画(线性)
    _progressAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.linear),
    );

    // 2. 旋转动画(0-2π)
    _rotationAnimation = Tween<double>(begin: 0.0, end: 2 * 3.1415926).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    // 3. 颜色动画(多颜色渐变)
    _colorAnimation = ColorTween(
      begin: Colors.blue,
      end: Colors.red,
    ).chain(
      ColorTween(begin: Colors.red, end: Colors.green),
    ).animate(
      CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
    );

    // 监听动画状态
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        // 动画完成后反转(从1.0→0.0)
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        // 动画回到初始状态后重新播放
        _controller.forward();
      }
    });
  }

  // 控制动画播放/暂停
  void _toggleAnimation() {
    if (_controller.isAnimating) {
      _controller.stop();
    } else {
      _controller.forward(); // 从0.0→1.0播放
    }
  }

  // 重置动画
  void _resetAnimation() {
    _controller.reset(); // 回到初始状态
  }

  @override
  void dispose() {
    // 释放动画控制器(必做!防止内存泄漏)
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('显式动画实战')),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            // 核心:AnimatedBuilder实现自定义动画
            AnimatedBuilder(
              // 动画对象(可传入多个动画的父控制器)
              animation: _controller,
              // 构建函数(只重建该区域,性能最优)
              builder: (context, child) {
                return Column(
                  children: [
                    // 旋转+颜色动画容器
                    Transform.rotate(
                      angle: _rotationAnimation.value,
                      child: Container(
                        width: 200,
                        height: 200,
                        color: _colorAnimation.value,
                        child: const Center(
                          child: Text(
                            '显式动画',
                            style: TextStyle(color: Colors.white, fontSize: 24),
                          ),
                        ),
                      ),
                    ),
                    const SizedBox(height: 30),
                    // 进度条动画
                    LinearProgressIndicator(
                      value: _progressAnimation.value,
                      backgroundColor: Colors.grey[200],
                      valueColor: AlwaysStoppedAnimation<Color>(
                        _colorAnimation.value ?? Colors.blue,
                      ),
                      minHeight: 10,
                    ),
                    const SizedBox(height: 10),
                    Text(
                      '进度:${(_progressAnimation.value * 100).toStringAsFixed(0)}%',
                      style: const TextStyle(fontSize: 18),
                    ),
                  ],
                );
              },
            ),
            const SizedBox(height: 40),
            // 操作按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _toggleAnimation,
                  child: Text(_controller.isAnimating ? '暂停动画' : '播放动画'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _resetAnimation,
                  style: ElevatedButton.styleFrom(foregroundColor: Colors.red),
                  child: const Text('重置动画'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

3.2 显式动画核心逻辑解析

1. 动画控制器(AnimationController)
  • 核心作用:控制动画的播放、暂停、反转、重置,管理动画的生命周期;
  • vsync:必须传入TickerProvider(通过混入SingleTickerProviderStateMixin实现),防止动画在页面不可见时继续运行,节省资源;
  • 生命周期管理:必须在dispose中释放控制器,否则会导致内存泄漏;
  • 常用方法
    • forward():从初始值播放到目标值;
    • reverse():从目标值反转到初始值;
    • stop():暂停动画;
    • reset():重置到初始状态;
    • repeat():重复播放动画。
2. 动画插值(Tween)
  • Tween:定义动画的取值范围,如Tween<double>(begin: 0.0, end: 1.0)表示从 0 到 1 的渐变;
  • ColorTween:颜色插值,支持从一个颜色渐变到另一个颜色;
  • Chain:多个 Tween 串联,实现多阶段插值(如蓝色→红色→绿色);
  • CurvedAnimation:为 Tween 添加动画曲线,控制动画的速度变化。
3. AnimatedBuilder(性能最优的显式动画方式)
  • 核心优势:只重建builder函数内的 Widget,不会重建整个父 Widget;
  • child 参数:如果有不参与动画的固定子 Widget,可通过child参数传入,避免重复重建;
  • 多动画管理:可传入AnimationController作为animation参数,监听所有基于该控制器的动画。

四、高阶 1:Hero 转场动画(页面间元素无缝过渡)

Hero 动画是 Flutter 中最具特色的转场动画之一,实现两个页面间同一元素的无缝过渡,比如点击商品图片跳转到详情页,图片会平滑地从列表位置过渡到详情页位置。

4.1 实现 Hero 动画(图片详情页转场)

第一步:列表页面(Hero 动画源)

dart

import 'package:flutter/material.dart';
import 'hero_detail_page.dart';

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

  // 模拟商品列表数据
  final List<Map<String, String>> _productList = [
    {
      'id': '1',
      'name': 'Flutter实战教程',
      'image': 'https://picsum.photos/200/200?random=1',
    },
    {
      'id': '2',
      'name': 'Dart编程指南',
      'image': 'https://picsum.photos/200/200?random=2',
    },
    {
      'id': '3',
      'name': '动画开发实战',
      'image': 'https://picsum.photos/200/200?random=3',
    },
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hero动画列表页')),
      body: GridView.builder(
        padding: const EdgeInsets.all(20),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2, // 两列
          crossAxisSpacing: 20,
          mainAxisSpacing: 20,
          childAspectRatio: 1.0,
        ),
        itemCount: _productList.length,
        itemBuilder: (context, index) {
          final product = _productList[index];
          return GestureDetector(
            onTap: () {
              // 跳转到详情页
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => HeroDetailPage(product: product),
                ),
              );
            },
            child: Column(
              children: [
                // 核心:Hero控件(必须设置唯一tag)
                Hero(
                  tag: product['id']!, // 唯一标识,详情页需相同
                  // flightShuttleBuilder:自定义过渡组件(可选)
                  flightShuttleBuilder: (
                    BuildContext flightContext,
                    Animation<double> animation,
                    HeroFlightDirection flightDirection,
                    BuildContext fromHeroContext,
                    BuildContext toHeroContext,
                  ) {
                    // 自定义过渡动画(缩放+渐变)
                    return ScaleTransition(
                      scale: animation.drive(
                        Tween<double>(begin: 0.8, end: 1.2).chain(
                          Tween<double>(begin: 1.2, end: 1.0),
                        ),
                      ),
                      child: FadeTransition(
                        opacity: animation,
                        child: Image.network(
                          product['image']!,
                          fit: BoxFit.cover,
                        ),
                      ),
                    );
                  },
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(16),
                    child: Image.network(
                      product['image']!,
                      width: 150,
                      height: 150,
                      fit: BoxFit.cover,
                    ),
                  ),
                ),
                const SizedBox(height: 10),
                Text(
                  product['name']!,
                  style: const TextStyle(fontSize: 16),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}
第二步:详情页面(Hero 动画目标)

dart

import 'package:flutter/material.dart';

class HeroDetailPage extends StatelessWidget {
  final Map<String, String> product;

  const HeroDetailPage({super.key, required this.product});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Column(
        children: [
          // 核心:Hero控件(tag必须与列表页一致)
          Hero(
            tag: product['id']!,
            child: Image.network(
              product['image']!,
              width: double.infinity,
              height: 300,
              fit: BoxFit.cover,
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(20),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    product['name']!,
                    style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 20),
                  const Text(
                    '这是商品的详细描述信息,Hero动画实现了图片从列表页到详情页的无缝过渡,'
                    '无需手动控制动画,只需给两个页面的相同元素添加相同tag的Hero控件即可。',
                    style: TextStyle(fontSize: 16, color: Colors.grey),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

4.2 Hero 动画核心逻辑解析

1. Hero 动画核心规则
  • 唯一 tag:两个页面的Hero控件必须设置相同的tag(通常用数据的唯一 ID),Flutter 通过 tag 识别需要过渡的元素;
  • 无需控制器:Hero 动画由 Flutter 自动管理,无需手动创建AnimationController
  • 过渡效果:默认是位置和尺寸的平滑过渡,可通过flightShuttleBuilder自定义过渡动画。
2. 自定义 Hero 过渡
  • flightShuttleBuilder:自定义过渡过程中的组件样式,可实现缩放、旋转、渐变等效果;
  • HeroFlightDirection:判断动画方向(push/pop),可根据方向设置不同的过渡效果;
  • transitionOnUserGestures:支持侧滑返回时的 Hero 动画(需配合PageRouteBuilder)。
3. 性能优化
  • 图片预加载:如果图片是网络图片,建议提前预加载,避免过渡过程中图片加载导致的卡顿;
  • 合理设置尺寸:Hero 动画会计算元素的尺寸和位置变化,避免过渡元素过大导致性能问题;
  • RepaintBoundary:如果过渡元素包含复杂的子 Widget,可包裹RepaintBoundary隔离重绘。

五、高阶 2:物理动画(模拟真实物理规律)

物理动画通过Simulation模拟真实世界的物理规律(如弹簧、重力、摩擦),让动画效果更自然、更贴近真实体验。

5.1 实现弹簧动画(拖拽小球回弹)

dart

import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

class PhysicsAnimationPage extends StatefulWidget {
  const PhysicsAnimationPage({super.key});

  @override
  State<PhysicsAnimationPage> createState() => _PhysicsAnimationPageState();
}

class _PhysicsAnimationPageState extends State<PhysicsAnimationPage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Offset> _animation;
  // 记录拖拽的初始位置
  Offset _dragStart = Offset.zero;
  // 小球的当前位置
  Offset _ballPosition = Offset.zero;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);
    // 监听动画更新,更新小球位置
    _controller.addListener(() {
      setState(() {
        _ballPosition = _animation.value;
      });
    });
  }

  // 处理拖拽开始
  void _onDragStart(DragStartDetails details) {
    _controller.stop(); // 停止当前动画
    _dragStart = details.globalPosition - _ballPosition;
  }

  // 处理拖拽中
  void _onDragUpdate(DragUpdateDetails details) {
    setState(() {
      _ballPosition = details.globalPosition - _dragStart;
    });
  }

  // 处理拖拽结束(触发物理动画)
  void _onDragEnd(DragEndDetails details) {
    // 创建弹簧模拟
    final spring = SpringSimulation(
      SpringDescription(
        mass: 1.0, // 质量
        stiffness: 100.0, // 刚度(越大越硬)
        damping: 15.0, // 阻尼(越大回弹越少)
      ),
      _ballPosition.dx, // 初始位置
      MediaQuery.of(context).size.width / 2 - 25, // 目标位置(屏幕中心)
      details.velocity.pixelsPerSecond.dx, // 初始速度(拖拽结束时的速度)
    );

    // 创建位置动画
    _animation = _controller.drive(
      Tween<Offset>(
        begin: _ballPosition,
        end: Offset(MediaQuery.of(context).size.width / 2 - 25, _ballPosition.dy),
      ).animate(
        Animation<double>(
          controller: _controller,
          simulation: spring,
        ),
      ),
    );

    // 播放动画
    _controller.forward(from: 0.0);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('物理动画(弹簧)')),
      body: Stack(
        children: [
          // 拖拽区域
          Positioned(
            left: _ballPosition.dx,
            top: _ballPosition.dy == 0 ? MediaQuery.of(context).size.height / 2 - 25 : _ballPosition.dy,
            child: GestureDetector(
              onPanStart: _onDragStart,
              onPanUpdate: _onDragUpdate,
              onPanEnd: _onDragEnd,
              child: Container(
                width: 50,
                height: 50,
                decoration: const BoxDecoration(
                  color: Colors.blue,
                  shape: BoxShape.circle,
                  boxShadow: [
                    BoxShadow(
                      color: Colors.black12,
                      blurRadius: 10,
                      spreadRadius: 5,
                    ),
                  ],
                ),
              ),
            ),
          ),
          // 提示文本
          const Positioned(
            bottom: 50,
            left: 0,
            right: 0,
            child: Center(
              child: Text(
                '拖拽小球体验弹簧物理动画',
                style: TextStyle(fontSize: 18, color: Colors.grey),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

5.2 物理动画核心逻辑解析

1. 核心物理模拟(Simulation)

Flutter 提供了多种物理模拟:

  • SpringSimulation:弹簧模拟(最常用),核心参数:
    • mass:质量(越大运动越慢);
    • stiffness:刚度(越大弹簧越硬,回弹越快);
    • damping:阻尼(越大回弹越少,最终停止越快);
  • GravitySimulation:重力模拟;
  • FrictionSimulation:摩擦模拟;
  • ClampingScrollSimulation:滚动阻尼模拟(类似列表滚动)。
2. 拖拽 + 物理动画结合
  • 拖拽事件处理
    • onPanStart:记录拖拽初始位置;
    • onPanUpdate:实时更新小球位置;
    • onPanEnd:获取拖拽结束时的速度,创建物理模拟并触发动画;
  • 速度传递:将拖拽结束时的速度作为物理模拟的初始速度,让动画更贴合真实的拖拽体验。
3. 关键优化点
  • 动画与拖拽的衔接:拖拽开始时停止当前动画,避免动画和拖拽冲突;
  • 屏幕边界处理:实际开发中需添加边界检测,防止小球超出屏幕;
  • 性能优化:物理模拟的计算量较大,避免在动画过程中进行复杂的计算。

六、动画开发避坑指南

  1. 内存泄漏
    • 显式动画的AnimationController必须在dispose中释放;
    • 避免在initState中创建无限循环的动画,忘记停止;
  2. 性能问题
    • 避免在AnimatedBuilderbuilder函数中创建新的 Widget(如Text('${value}')),可提前缓存;
    • 复杂动画使用RepaintBoundary隔离重绘区域;
    • 避免同时进行多个高开销的动画(如大量元素的旋转 + 缩放);
  3. 动画卡顿
    • 网络图片提前预加载,避免动画过程中加载图片;
    • 减少动画过程中的布局计算(如LayoutBuilder);
    • 使用const构造函数创建静态子 Widget;
  4. Hero 动画坑点
    • 两个页面的Hero tag 必须完全一致;
    • 避免 Hero 元素包含复杂的交互控件(如按钮);
    • 侧滑返回时的 Hero 动画需手动配置transitionOnUserGestures
  5. 物理动画坑点
    • 弹簧参数需反复调试,避免过度回弹或僵硬;
    • 拖拽速度的单位是pixelsPerSecond,需注意单位转换。

七、总结

Flutter 动画开发的学习路径是 “隐式动画→显式动画→转场动画→物理动画”,核心原则是 “按需选型、性能优先、贴近真实”:

  1. 简单动效(显隐、尺寸变化):使用隐式动画(AnimatedContainer/AnimatedOpacity),快速高效;
  2. 复杂交互动画(进度条、自定义曲线):使用显式动画(AnimationController+AnimatedBuilder),灵活可控;
  3. 页面转场:使用 Hero 动画,实现元素无缝过渡;
  4. 真实交互体验(拖拽、回弹):使用物理动画,模拟真实物理规律。

动画开发的核心不是 “炫技”,而是 “提升用户体验”—— 一个好的动画应该是 “润物细无声” 的,既让交互更生动,又不影响性能和使用。比如按钮点击的微小缩放、列表加载的淡入动画、页面切换的平滑过渡,这些细节能让应用的体验提升一个档次。希望本文的实战案例和原理解析,能让你避开动画开发的 “坑”,写出既严谨又生动的 Flutter 动画代码。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐