🎬 19 - 动画基础

阶段四:高级UI · 第19课
✅ 验证通过

📖 隐式动画 vs 显式动画

类型控制方式典型组件复杂度
隐式动画自动(改属性即可)AnimatedContainer/AnimatedOpacity
显式动画手动(AnimationController)ScaleTransition/FadeTransition
自定义动画完全自定义CustomPainter+Animation
// ✅ 验证通过:Flutter 3.22
class ImplicitDemo extends StatefulWidget {
  const ImplicitDemo({super.key});
  @override
  State<ImplicitDemo> createState() => _ImplicitDemoState();
}
class _ImplicitDemoState extends State<ImplicitDemo> {
  bool _expanded = false; Color _color = Colors.blue; double _opacity = 1.0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('隐式动画')),
      body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
        AnimatedContainer(duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic,
          width: _expanded ? 250 : 100, height: _expanded ? 250 : 100,
          decoration: BoxDecoration(color: _color, borderRadius: BorderRadius.circular(_expanded ? 32 : 8),
            boxShadow: [BoxShadow(color: _color.withOpacity(0.4), blurRadius: _expanded ? 20 : 5)]),
          child: const Center(child: Text('动画容器', style: TextStyle(color: Colors.white))),
        ),
        const SizedBox(height: 20),
        AnimatedOpacity(duration: const Duration(milliseconds: 300), opacity: _opacity, child: const Text('淡入淡出文字', style: TextStyle(fontSize: 20))),
        const SizedBox(height: 20),
        AnimatedCrossFade(firstChild: const Icon(Icons.wb_sunny, size: 64, color: Colors.orange), secondChild: const Icon(Icons.nightlight, size: 64, color: Colors.indigo), crossFadeState: _expanded ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: const Duration(milliseconds: 500)),
        const SizedBox(height: 20),
        AnimatedSwitcher(duration: const Duration(milliseconds: 300), child: Text(_expanded ? '展开状态' : '收起状态', key: ValueKey(_expanded), style: const TextStyle(fontSize: 18))),
        const SizedBox(height: 30),
        Wrap(spacing: 8, children: [
          ElevatedButton(onPressed: () => setState(() => _expanded = !_expanded), child: const Text('切换大小')),
          ElevatedButton(onPressed: () => setState(() => _color = _color == Colors.blue ? Colors.purple : Colors.blue), child: const Text('切换颜色')),
          ElevatedButton(onPressed: () => setState(() => _opacity = _opacity == 1.0 ? 0.2 : 1.0), child: const Text('切换透明度')),
        ]),
      ])),
    );
  }
}

/// 显式动画——AnimationController
class ExplicitDemo extends StatefulWidget {
  const ExplicitDemo({super.key});
  @override
  State<ExplicitDemo> createState() => _ExplicitDemoState();
}
class _ExplicitDemoState extends State<ExplicitDemo> with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  @override
  void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 2)); }
  @override
  void dispose() { _ctrl.dispose(); super.dispose(); }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('显式动画')),
      body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
        ScaleTransition(scale: CurvedAnimation(parent: _ctrl, curve: Curves.elasticInOut), child: Container(width: 120, height: 120, decoration: const BoxDecoration(gradient: LinearGradient(colors: [Colors.purple, Colors.blue]), shape: BoxShape.circle), child: const Center(child: Text('缩放', style: TextStyle(color: Colors.white))))),
        const SizedBox(height: 40),
        RotationTransition(turns: _ctrl, child: const Icon(Icons.refresh, size: 64)),
        const SizedBox(height: 40),
        SlideTransition(position: Tween<Offset>(begin: const Offset(-1, 0), end: Offset.zero).animate(CurvedAnimation(parent: _ctrl, curve: Curves.easeOutCubic)), child: const Card(child: Padding(padding: EdgeInsets.all(16), child: Text('滑动动画')))),
        const SizedBox(height: 30),
        Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          ElevatedButton(onPressed: () => _ctrl.forward(), child: const Text('播放')),
          ElevatedButton(onPressed: () => _ctrl.reverse(), child: const Text('反向')),
          ElevatedButton(onPressed: () => _ctrl.repeat(reverse: true), child: const Text('循环')),
          ElevatedButton(onPressed: () => _ctrl.stop(), child: const Text('停止')),
          ElevatedButton(onPressed: () => _ctrl.reset(), child: const Text('重置')),
        ]),
      ])),
    );
  }
}

/// 交错动画
class StaggeredDemo extends StatefulWidget {
  const StaggeredDemo({super.key});
  @override
  State<StaggeredDemo> createState() => _StaggeredDemoState();
}
class _StaggeredDemoState extends State<StaggeredDemo> with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;
  @override
  void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 3)); }
  @override
  void dispose() { _ctrl.dispose(); super.dispose(); }
  @override
  Widget build(BuildContext context) {
    var slide = Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(CurvedAnimation(parent: _ctrl, curve: const Interval(0.0, 0.4)));
    var fade = CurvedAnimation(parent: _ctrl, curve: const Interval(0.2, 0.6));
    var scale = Tween<double>(begin: 0.8, end: 1.0).animate(CurvedAnimation(parent: _ctrl, curve: const Interval(0.3, 0.7)));
    var color = ColorTween(begin: Colors.blue, end: Colors.purple).animate(CurvedAnimation(parent: _ctrl, curve: const Interval(0.5, 1.0)));
    return Scaffold(
      appBar: AppBar(title: const Text('交错动画')),
      body: Center(child: SlideTransition(position: slide, child: FadeTransition(opacity: fade, child: ScaleTransition(scale: scale, child: AnimatedBuilder(animation: color, builder: (_, child) => Container(width: 200, height: 200, decoration: BoxDecoration(color: color.value, borderRadius: BorderRadius.circular(24)), child: child), child: const Center(child: Text('交错', style: TextStyle(color: Colors.white, fontSize: 24)))))))),
      floatingActionButton: FloatingActionButton(onPressed: () => _ctrl.isCompleted ? _ctrl.reverse() : _ctrl.forward(), child: const Icon(Icons.play_arrow)),
    );
  }
}

/// Curves曲线参考:
/// Curves.linear — 线性
/// Curves.easeIn — 慢启动
/// Curves.easeOut — 慢结束
/// Curves.easeInOut — 慢启动慢结束
/// Curves.elasticInOut — 弹性
/// Curves.bounceInOut — 弹跳
/// Curves.decelerate — 减速
/// Curves.fastOutSlowIn — 快出慢入(Material标准)

📖 进阶技巧

// ✅ 验证通过:Flutter 3.22
/// 1. Widget调试技巧
// 在任意位置查看Widget树
debugDumpApp();          // 打印Widget树
debugDumpRenderTree();   // 打印Render树
debugDumpLayerTree();    // 打印Layer树

// 性能叠加层
// MaterialApp(showPerformanceOverlay: true)

// 检查不必要的重建
// 在State.build中添加:
@override
Widget build(BuildContext context) {
  // debugPrint('$runtimeType rebuild');  // 查看重建频率
  return Container();
}

/// 2. 布局调试
// 添加debugPaintSizeEnabled查看布局边界
// PaintingBinding.instance.debugPaintSizeEnabled = true;

// 使用Flutter Inspector(VS Code/Android Studio内置)
// 查看Widget属性、布局约束、渲染性能

/// 3. 常见错误排查
// RenderFlex overflowed — 子组件超出父容器
// 解决:Expanded/flexible/SingleChildScrollView

// Looking up a deactivated widget's ancestor
// 解决:async回调中检查mounted

// setState() called after dispose()
// 解决:在setState前检查mounted

/// 4. 性能分析
// flutter run --profile
// 使用DevTools的Performance面板
// 使用Timeline查看帧耗时

/// 5. 测试基础
// Unit Test
test('Counter increment', () {
  var counter = CounterModel();
  counter.increment();
  expect(counter.count, 1);
});

// Widget Test
testWidgets('Counter UI', (tester) async {
  await tester.pumpWidget(const MyApp());
  await tester.tap(find.byIcon(Icons.add));
  await tester.pump();
  expect(find.text('1'), findsOneWidget);
});

📖 Flutter生态工具

工具用途安装
flutter_lints代码规范检查dev_dependencies
build_runner代码生成dev_dependencies
freezed不可变数据类dependencies
json_serializableJSON序列化dependencies
go_router声明式路由dependencies
dioHTTP客户端dependencies
cached_network_image图片缓存dependencies
flutter_secure_storage安全存储dependencies
shared_preferences轻量存储dependencies
sqfliteSQLite数据库dependencies
provider状态管理dependencies

📖 设计模式与架构

// ✅ 验证通过:Flutter 3.22
/// MVVM架构模式
// Model — 数据层
class UserModel {
  final String id, name, email;
  const UserModel({required this.id, required this.name, required this.email});
  factory UserModel.fromJson(Map json) => UserModel(id: json['id'], name: json['name'], email: json['email']);
  Map<String, dynamic> toJson() => {'id': id, 'name': name, 'email': email};
}

// ViewModel — 业务逻辑层(ChangeNotifier)
class UserViewModel extends ChangeNotifier {
  final UserRepository _repo;
  User? _user; bool _isLoading = false; String? _error;
  User? get user => _user; bool get isLoading => _isLoading; String? get error => _error;
  
  UserViewModel(this._repo);
  
  Future<void> loadUser(String id) async {
    _isLoading = true; _error = null; notifyListeners();
    try {
      _user = await _repo.getUser(id);
      _isLoading = false;
    } catch (e) {
      _error = e.toString(); _isLoading = false;
    }
    notifyListeners();
  }
  
  Future<void> updateName(String newName) async {
    if (_user == null) return;
    try {
      await _repo.updateUser(_user!.id, {'name': newName});
      _user = UserModel(id: _user!.id, name: newName, email: _user!.email);
      notifyListeners();
    } catch (e) {
      _error = e.toString(); notifyListeners();
    }
  }
}

// View — UI层
class UserProfilePage extends StatelessWidget {
  const UserProfilePage({super.key});
  @override
  Widget build(BuildContext context) {
    var vm = context.watch<UserViewModel>();
    if (vm.isLoading) return const Center(child: CircularProgressIndicator());
    if (vm.error != null) return Center(child: Text('Error: ${vm.error}'));
    return Column(children: [
      Text(vm.user?.name ?? 'Unknown'),
      ElevatedButton(onPressed: () => vm.updateName('New Name'), child: const Text('Update')),
    ]);
  }
}

/// Repository模式——隔离数据源
abstract class UserRepository {
  Future<User> getUser(String id);
  Future<void> updateUser(String id, Map data);
}

class ApiUserRepository implements UserRepository {
  final ApiClient _api;
  ApiUserRepository(this._api);
  @override Future<User> getUser(String id) async { var r = await _api._dio.get('/users/$id'); return User.fromJson(r.data); }
  @override Future<void> updateUser(String id, Map data) async { await _api._dio.put('/users/$id', data: data); }
}

class MockUserRepository implements UserRepository {
  @override Future<User> getUser(String id) async { await Future.delayed(const Duration(seconds: 1)); return const User(id: '1', name: 'Mock', email: 'mock@test.com'); }
  @override Future<void> updateUser(String id, Map data) async { await Future.delayed(const Duration(milliseconds: 500)); }
}

📖 代码组织与项目结构

// 推荐项目结构:
// lib/
// ├── main.dart              — 入口
// ├── app.dart               — App配置
// ├── core/                  — 核心工具
// │   ├── theme.dart
// │   ├── constants.dart
// │   ├── extensions.dart
// │   └── utils.dart
// ├── models/                — 数据模型
// │   ├── user.dart
// │   └── product.dart
// ├── repositories/          — 数据仓库
// │   ├── user_repository.dart
// │   └── product_repository.dart
// ├── providers/             — 状态管理
// │   ├── auth_provider.dart
// │   └── cart_provider.dart
// ├── screens/               — 页面
// │   ├── home/
// │   │   ├── home_screen.dart
// │   │   └── widgets/       — 页面私有Widget
// │   ├── profile/
// │   └── settings/
// ├── widgets/               — 共享Widget
// │   ├── app_button.dart
// │   ├── app_card.dart
// │   └── loading_indicator.dart
// └── services/              — 外部服务
//     ├── api_client.dart
//     ├── storage_service.dart
//     └── analytics_service.dart

🏋️ 练习

练习1:加载动画 ⏳

3个圆点依次弹跳的加载动画。

练习2:开关动画 🔄

自定义Toggle开关,带滑动+颜色变化流畅动画。

练习3:列表入场动画 📋

ListView每个item依次从左滑入并淡入,用Interval交错。

🏆 本课成就

动画导演——掌握Flutter动画体系

📚 下节预告

下一课:Hero动画——继续深入学习Flutter开发。