Flutter flutter_animate 动画的鸿蒙化适配与实战指南

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


各位小伙伴们好呀!👋 我是那个上海某高校的大一计算机学生,继续来给大家分享 Flutter for OpenHarmony 开发的学习心得!

今天要聊的是 flutter_animate 动画!🎬

一个 App 如果没有动画,就像一盘菜没有调料,总觉得少了点什么。用户滑动、点击、页面切换… 有了动画才让这些交互变得生动有趣!

之前用 Flutter 原生的动画 API,写起来特别复杂,要用 AnimationController、Tween、Curve… 一堆概念 😵。后来发现了 flutter_animate 这个库,我的天,简直是救星!代码简洁到爆,今天必须分享给大家!


一、功能引入介绍 🎨

1.1 为什么需要动画?

动画不是花架子,它有实际作用:

  • 🎯 引导注意力:新用户看到动画引导,能更快理解功能
  • 😊 提升体验:流畅的动画让 App 看起来更专业
  • 📚 传达状态:加载中、成功、失败… 动画比文字更直观
  • 🎮 增加趣味:好的动画让用户更愿意使用 App

1.2 flutter_animate 的优势

对比项 原生动画 API flutter_animate
代码量 少 ✅
学习成本 低 ✅
动画类型 基础 丰富 ✅
组合能力 一般 强大 ✅

二、环境与依赖配置 🔧

2.1 pubspec.yaml 依赖

dependencies:
  flutter:
    sdk: flutter
  
  # ========== 动画 ==========
  flutter_animate: ^4.5.0

三、分步实现完整代码 🚀

3.1 基础动画效果

flutter_animate 的使用非常简单,只需要在 Widget 后面链式调用 .animate()

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('动画演示')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 淡入动画
            Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ).animate().fadeIn(duration: 500.ms),
            
            const SizedBox(height: 20),
            
            // 滑动进入 + 淡入
            Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ).animate().slideX(begin: -0.5, end: 0).fadeIn(),
            
            const SizedBox(height: 20),
            
            // 缩放动画
            Container(
              width: 100,
              height: 100,
              color: Colors.green,
            ).animate().scale(begin: const Offset(0, 0), end: const Offset(1, 1)),
          ],
        ),
      ),
    );
  }
}

3.2 二维码页面的动画效果

这是项目中实际使用的动画实现:

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';

class QRCodePage extends StatefulWidget {
  final String? initialData;
  final String? title;

  const QRCodePage({
    super.key,
    this.initialData,
    this.title,
  });

  
  State<QRCodePage> createState() => _QRCodePageState();
}

class _QRCodePageState extends State<QRCodePage> {
  final TextEditingController _dataController = TextEditingController();
  String _currentData = '';
  Color _selectedColor = const Color(0xFF6366F1);

  final List<Color> _availableColors = [
    const Color(0xFF6366F1),
    const Color(0xFF8B5CF6),
    const Color(0xFFEC4899),
    const Color(0xFFEF4444),
    const Color(0xFFF97316),
    const Color(0xFF10B981),
    const Color(0xFF06B6D4),
    const Color(0xFF3B82F6),
  ];

  
  void initState() {
    super.initState();
    if (widget.initialData != null) {
      _currentData = widget.initialData!;
      _dataController.text = widget.initialData!;
    } else {
      _currentData = 'https://my-ohos-app.com';
      _dataController.text = _currentData;
    }
  }

  void _updateQRCode() {
    setState(() {
      _currentData = _dataController.text.trim();
      if (_currentData.isEmpty) {
        _currentData = 'https://my-ohos-app.com';
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF8FAFC),
      appBar: AppBar(
        title: Text(widget.title ?? '生成二维码'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // 二维码预览 - 带动画效果
            _buildQRPreview()
                .animate()
                .fadeIn(duration: 300.ms)  // 淡入
                .scale(
                  begin: const Offset(0.9, 0.9),  // 从 90% 开始
                  end: const Offset(1, 1),         // 到 100%
                ),
            
            const SizedBox(height: 24),
            
            // 内容输入 - 带滑动动画
            _buildDataInput()
                .animate()
                .fadeIn(delay: 100.ms, duration: 300.ms)  // 延迟 100ms 淡入
                .slideY(begin: 0.2, end: 0),              // 从下往上滑入
            
            const SizedBox(height: 24),
            
            // 颜色选择器
            _buildColorPicker()
                .animate()
                .fadeIn(delay: 200.ms, duration: 300.ms)
                .slideY(begin: 0.2, end: 0),
            
            const SizedBox(height: 24),
            
            // 快捷操作
            _buildQuickActions()
                .animate()
                .fadeIn(delay: 300.ms, duration: 300.ms)
                .slideY(begin: 0.2, end: 0),
          ],
        ),
      ),
    );
  }

  /// 二维码预览组件
  Widget _buildQRPreview() {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(20),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.05),
            blurRadius: 20,
            offset: const Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        children: [
          Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(16),
              border: Border.all(color: Colors.grey.shade200),
            ),
            child: QrImageView(
              data: _currentData,
              version: QrVersions.auto,
              size: 200,
              eyeStyle: QrEyeStyle(
                eyeShape: QrEyeShape.square,
                color: _selectedColor,
              ),
              dataModuleStyle: QrDataModuleStyle(
                dataModuleShape: QrDataModuleShape.square,
                color: _selectedColor,
              ),
            ),
          ),
          const SizedBox(height: 16),
          Text(
            _currentData.length > 30 
                ? '${_currentData.substring(0, 30)}...' 
                : _currentData,
            style: TextStyle(color: Colors.grey[600], fontSize: 12),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }

  /// 数据输入组件
  Widget _buildDataInput() {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: _cardDecoration(),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          TextField(
            controller: _dataController,
            maxLines: 3,
            decoration: InputDecoration(
              hintText: '输入文本、链接或任意数据',
              filled: true,
              fillColor: const Color(0xFFF8FAFC),
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
                borderSide: BorderSide.none,
              ),
            ),
            onChanged: (_) => _updateQRCode(),
          ),
        ],
      ),
    );
  }

  /// 颜色选择器
  Widget _buildColorPicker() {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: _cardDecoration(),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('颜色', style: TextStyle(fontWeight: FontWeight.bold)),
          const SizedBox(height: 16),
          Wrap(
            spacing: 12,
            children: _availableColors.map((color) {
              final isSelected = _selectedColor == color;
              return GestureDetector(
                onTap: () => setState(() => _selectedColor = color),
                child: Container(
                  width: 44,
                  height: 44,
                  decoration: BoxDecoration(
                    color: color,
                    shape: BoxShape.circle,
                    border: Border.all(
                      color: isSelected ? Colors.white : Colors.transparent,
                      width: 3,
                    ),
                    boxShadow: isSelected
                        ? [
                            BoxShadow(
                              color: color.withValues(alpha: 0.5),
                              blurRadius: 8,
                              spreadRadius: 2,
                            ),
                          ]
                        : null,
                  ),
                  child: isSelected
                      ? const Icon(Icons.check, color: Colors.white, size: 20)
                      : null,
                ),
              );
            }).toList(),
          ),
        ],
      ),
    );
  }

  /// 快捷操作
  Widget _buildQuickActions() {
    return Container(
      padding: const EdgeInsets.all(20),
      decoration: _cardDecoration(),
      child: Row(
        children: [
          Expanded(
            child: ElevatedButton.icon(
              onPressed: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('已复制')),
                );
              },
              icon: const Icon(Icons.copy),
              label: const Text('复制'),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: ElevatedButton.icon(
              onPressed: () async {
                // 分享逻辑
                await Share.share(_currentData);
              },
              icon: const Icon(Icons.share),
              label: const Text('分享'),
            ),
          ),
        ],
      ),
    );
  }

  BoxDecoration _cardDecoration() {
    return BoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(16),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withValues(alpha: 0.05),
          blurRadius: 10,
          offset: const Offset(0, 2),
        ),
      ],
    );
  }
}

3.3 高级动画效果

import 'package:flutter_animate/flutter_animate.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          // ============ 1. 组合动画 ============
          _buildTitle('组合动画'),
          Container(
            width: 100,
            height: 100,
            color: Colors.blue,
          )
              .animate()
              .fadeIn(duration: 500.ms)      // 先淡入
              .slideX(begin: -0.3, end: 0)    // 再左滑
              .scale(begin: const Offset(0.8, 0.8), end: const Offset(1, 1)), // 最后放大

          const SizedBox(height: 30),

          // ============ 2. 延迟动画 ============
          _buildTitle('延迟动画(依次出现)'),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: List.generate(5, (index) {
              return Container(
                width: 50,
                height: 50,
                color: Colors.primaries[index],
              )
                  .animate()
                  .fadeIn(delay: (index * 100).ms)  // 每个延迟 100ms
                  .scale();
            }),
          ),

          const SizedBox(height: 30),

          // ============ 3. 摇晃效果 ============
          _buildTitle('摇晃效果'),
          Container(
            width: 100,
            height: 100,
            color: Colors.orange,
          )
              .animate(onPlay: (controller) => controller.repeat())
              .shake(hz: 2, duration: 1.seconds),

          const SizedBox(height: 30),

          // ============ 4. 脉冲效果 ============
          _buildTitle('脉冲效果'),
          Container(
            width: 100,
            height: 100,
            color: Colors.red,
          )
              .animate(onPlay: (controller) => controller.repeat(reverse: true))
              .scale(
                begin: const Offset(1, 1),
                end: const Offset(1.1, 1.1),
                duration: 500.ms,
              ),

          const SizedBox(height: 30),

          // ============ 5. 滑动效果 ============
          _buildTitle('各种滑动效果'),
          Row(
            children: [
              Expanded(child: _buildSlideCard(Icons.arrow_upward, '上', Colors.blue)),
              const SizedBox(width: 8),
              Expanded(child: _buildSlideCard(Icons.arrow_downward, '下', Colors.green)),
              const SizedBox(width: 8),
              Expanded(child: _buildSlideCard(Icons.arrow_back, '左', Colors.orange)),
              const SizedBox(width: 8),
              Expanded(child: _buildSlideCard(Icons.arrow_forward, '右', Colors.purple)),
            ],
          ),

          const SizedBox(height: 30),

          // ============ 6. 翻转效果 ============
          _buildTitle('翻转效果'),
          Container(
            width: 100,
            height: 100,
            color: Colors.teal,
          )
              .animate()
              .flip(duration: 800.ms),

          const SizedBox(height: 30),

          // ============ 7. 颜色渐变 ============
          _buildTitle('颜色动画'),
          Container(
            width: 100,
            height: 100,
            color: Colors.red,
          )
              .animate(onPlay: (controller) => controller.repeat(reverse: true))
              .tint(
                color: Colors.blue.withValues(alpha: 0.5),
                duration: 2.seconds,
              ),
        ],
      ),
    );
  }

  Widget _buildTitle(String title) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 16),
      child: Text(
        title,
        style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
      ),
    );
  }

  Widget _buildSlideCard(IconData icon, String label, Color color) {
    return Container(
      height: 80,
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(icon, color: Colors.white, size: 30),
          Text(label, style: const TextStyle(color: Colors.white)),
        ],
      ),
    )
        .animate()
        .fadeIn()
        .slideY(begin: 0.5, end: 0, curve: Curves.easeOutBack);
  }
}

3.4 列表动画

让列表的每一项都有入场动画:

import 'package:flutter_animate/flutter_animate.dart';

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

  
  Widget build(BuildContext context) {
    final items = List.generate(20, (index) => '商品 ${index + 1}');

    return Scaffold(
      appBar: AppBar(title: const Text('列表动画')),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            leading: CircleAvatar(
              backgroundColor: Colors.primaries[index % Colors.primaries.length],
              child: Text('${index + 1}'),
            ),
            title: Text(items[index]),
            subtitle: Text('这是第 ${index + 1} 个商品'),
          )
              // 每个 item 依次延迟 50ms 淡入并从左滑入
              .animate()
              .fadeIn(delay: (index * 50).ms)
              .slideX(begin: 0.3, end: 0, delay: (index * 50).ms);
        },
      ),
    );
  }
}

3.5 骨架屏动画

加载数据时显示骨架屏动画:

import 'package:shimmer/shimmer.dart';
import 'package:flutter_animate/flutter_animate.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        padding: const EdgeInsets.all(16),
        itemCount: 5,
        itemBuilder: (context, index) {
          return Card(
            margin: const EdgeInsets.only(bottom: 16),
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // 图片骨架
                  Container(
                    height: 150,
                    decoration: BoxDecoration(
                      color: Colors.grey[300],
                      borderRadius: BorderRadius.circular(8),
                    ),
                  )
                      .animate(onPlay: (c) => c.repeat())
                      .shimmer(duration: 1.seconds, color: Colors.grey.shade100),
                  
                  const SizedBox(height: 12),
                  
                  // 标题骨架
                  Container(
                    height: 20,
                    width: 200,
                    decoration: BoxDecoration(
                      color: Colors.grey[300],
                      borderRadius: BorderRadius.circular(4),
                    ),
                  )
                      .animate(onPlay: (c) => c.repeat())
                      .shimmer(duration: 1.2.seconds, color: Colors.grey.shade100),
                  
                  const SizedBox(height: 8),
                  
                  // 描述骨架
                  Container(
                    height: 16,
                    width: double.infinity,
                    decoration: BoxDecoration(
                      color: Colors.grey[300],
                      borderRadius: BorderRadius.circular(4),
                    ),
                  )
                      .animate(onPlay: (c) => c.repeat())
                      .shimmer(duration: 1.4.seconds, color: Colors.grey.shade100),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

四,开发踩坑与挫折 😤

4.1 踩坑一:动画不执行

问题描述
动画没有任何反应。

排查过程

  1. 检查是否在 StatelessWidget 中使用
  2. 检查 .animate() 是否调用了

解决方案

// ❌ 错误
Container(color: Colors.red).fadeIn();

// ✅ 正确
Container(color: Colors.red).animate().fadeIn();

4.2 踩坑二:动画只执行一次

问题描述
动画播放一次后就不再播放了。

解决方案
使用 onPlay 回调中的 controller.repeat()

Container(color: Colors.red)
    .animate(onPlay: (controller) => controller.repeat())
    .shake();

4.3 踩坑三:动画延迟太久

问题描述
设置了 delay,但动画看起来卡顿。

解决方案
动画延迟总和不宜过长,建议每个 item 延迟 30-50ms:

// ✅ 推荐:每个 item 延迟 50ms
item.animate()
    .fadeIn(delay: (index * 50).ms)
    .slideY(begin: 0.3, end: 0);

五、最终实现效果 📸

(此处附鸿蒙设备上成功运行的截图)

在这里插入图片描述

六、个人学习总结 📝

通过 flutter_animate 的学习,我收获了很多:

  1. ✅ 动画不需要复杂的 AnimationController
  2. ✅ 链式调用让代码更优雅
  3. ✅ 各种预设动画效果让 App 更生动

flutter_animate 真的是 Flutter 动画的最佳选择!强烈推荐给大家!

作者:上海某高校大一学生,Flutter 爱好者
发布时间:2026年4月

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐