在这里插入图片描述

动画能让App更生动、交互更流畅。今天我们来讲解Flutter动画的实现方式,包括隐式动画、显式动画和页面转场动画。Flutter动画分为两类:隐式动画简单易用,显式动画更灵活可控。

隐式动画

隐式动画使用AnimatedXxx系列组件,只需要改变属性值,动画自动执行。下面是收藏按钮的动画效果:

class FavoriteButton extends StatefulWidget {
  final bool isFavorite;
  final VoidCallback onTap;

  const FavoriteButton({
    super.key,
    required this.isFavorite,
    required this.onTap,
  });

  
  State<FavoriteButton> createState() => _FavoriteButtonState();
}

class _FavoriteButtonState extends State<FavoriteButton> {
  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        padding: const EdgeInsets.all(8),
        decoration: BoxDecoration(
          color: widget.isFavorite 
            ? Colors.red.withOpacity(0.1) 
            : Colors.transparent,
          shape: BoxShape.circle,
        ),
        child: AnimatedSwitcher(
          duration: const Duration(milliseconds: 200),
          child: Icon(
            widget.isFavorite ? Icons.favorite : Icons.favorite_border,
            key: ValueKey(widget.isFavorite),
            color: widget.isFavorite ? Colors.red : Colors.grey,
          ),
        ),
      ),
    );
  }
}

这个收藏按钮组件展示了隐式动画的典型用法。AnimatedContainer会在属性变化时自动执行平滑过渡动画,比如背景色从透明变为淡红色。AnimatedSwitcher则负责在图标切换时添加过渡效果,通过ValueKey来标识不同状态。整个动画时长设为200毫秒,既不会太快让用户察觉不到,也不会太慢影响操作流畅度。这种方式代码简洁,非常适合简单的状态切换场景。

点击缩放效果

class ScaleOnTap extends StatefulWidget {
  final Widget child;
  final VoidCallback? onTap;

  const ScaleOnTap({
    super.key,
    required this.child,
    this.onTap,
  });

  
  State<ScaleOnTap> createState() => _ScaleOnTapState();
}

class _ScaleOnTapState extends State<ScaleOnTap> {
  bool _isPressed = false;

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapDown: (_) => setState(() => _isPressed = true),
      onTapUp: (_) => setState(() => _isPressed = false),
      onTapCancel: () => setState(() => _isPressed = false),
      onTap: widget.onTap,
      child: AnimatedScale(
        scale: _isPressed ? 0.95 : 1.0,
        duration: const Duration(milliseconds: 100),
        child: widget.child,
      ),
    );
  }
}

这是一个通用的点击缩放包装组件,可以给任何Widget添加按压反馈效果。通过监听onTapDownonTapUp事件来控制缩放状态,按下时缩小到0.95倍,松开后恢复原大小。AnimatedScale会自动处理缩放过渡动画,100毫秒的时长让反馈既及时又自然。这种微交互虽然简单,但能显著提升用户的操作体验,让界面感觉更有"质感"。

显式动画

显式动画使用AnimationController手动控制,适合复杂的动画场景。下面是启动页的淡入动画:

class _SplashPageState extends State<SplashPage> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _fadeAnimation;
  late Animation<double> _scaleAnimation;

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 800),
      vsync: this,
    );
    
    _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeOut),
    );
    
    _scaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeOut),
    );
    
    _controller.forward();
    
    Future.delayed(const Duration(seconds: 2), () {
      Get.off(() => const MainPage());
    });
  }

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
            colors: [Color(0xFF07C160), Color(0xFF06AD56)],
          ),
        ),
        child: Center(
          child: FadeTransition(
            opacity: _fadeAnimation,
            child: ScaleTransition(
              scale: _scaleAnimation,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Container(
                    width: 100,
                    height: 100,
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(20),
                    ),
                    child: const Icon(Icons.swap_horiz, size: 60, color: Color(0xFF07C160)),
                  ),
                  const SizedBox(height: 24),
                  const Text('闲置换', style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.white)),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

启动页动画展示了显式动画的完整流程。首先创建AnimationController作为动画的"指挥官",通过Tween定义动画的起止值。CurvedAnimation添加了easeOut缓动曲线,让动画开始快结束慢,更符合物理直觉。Logo和文字同时执行淡入和缩放两个动画,从0.8倍放大到1倍,透明度从0到1。记得在dispose中释放Controller,避免内存泄漏。

列表项动画

商品列表加载时的错落动画效果:

class AnimatedListItem extends StatefulWidget {
  final int index;
  final Widget child;

  const AnimatedListItem({super.key, required this.index, required this.child});

  
  State<AnimatedListItem> createState() => _AnimatedListItemState();
}

class _AnimatedListItemState extends State<AnimatedListItem>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _fadeAnimation;
  late Animation<Offset> _slideAnimation;

  
  void initState() {
    super.initState();
    _controller = AnimationController(duration: const Duration(milliseconds: 400), vsync: this);
    _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeOut),
    );
    _slideAnimation = Tween<Offset>(begin: const Offset(0, 0.2), end: Offset.zero)
        .animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
    Future.delayed(Duration(milliseconds: widget.index * 50), () {
      if (mounted) _controller.forward();
    });
  }

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

  
  Widget build(BuildContext context) {
    return FadeTransition(
      opacity: _fadeAnimation,
      child: SlideTransition(position: _slideAnimation, child: widget.child),
    );
  }
}

列表项动画的关键在于"错落感"。每个item根据自己的index延迟执行动画,第一个立即开始,第二个延迟50ms,第三个延迟100ms,以此类推。这样商品卡片会像瀑布一样依次出现,比同时出现更有层次感。SlideTransition让卡片从下方滑入,配合淡入效果,整体观感非常流畅。注意检查mounted状态,避免组件销毁后还执行动画导致报错。

页面转场与Hero动画

GetX支持多种页面转场效果:

Get.to(() => const ProductDetailPage(productId: 1), transition: Transition.rightToLeft);
Get.to(() => const PublishPage(), transition: Transition.downToUp);
Get.to(() => const SearchPage(), transition: Transition.fade);

Hero动画实现商品图片的"飞行"效果:

// 列表页
Hero(tag: 'product_image_${product['id']}', child: Image.network(product['image']))
// 详情页
Hero(tag: 'product_image_${widget.productId}', child: Image.network(product['image']))

页面转场动画能让导航更自然,rightToLeft适合常规页面跳转,downToUp适合弹出式页面如发布页,fade适合搜索等轻量级页面。Hero动画是Flutter的特色功能,只需在两个页面的Widget上设置相同的tag,系统会自动计算位置差异并执行平滑的飞行动画,非常适合商品图片从列表"飞"到详情页的场景。

小结

这篇讲解了Flutter动画的核心实现方式。隐式动画适合简单场景,显式动画适合复杂控制,Hero动画让页面切换更流畅。适当的动画能让App更生动,但也要注意不要过度使用,保持60fps的流畅体验才是关键。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐