在这里插入图片描述

案例概述

本案例展示如何使用 AnimatedContainer 创建平滑的动画效果。AnimatedContainer 是 Flutter 中最常用的动画组件之一,它自动化了属性变化的动画过程。当容器的大小、颜色、圆角等属性变化时,AnimatedContainer 会自动生成平滑的过渡动画。

在现代应用中,AnimatedContainer 广泛应用于:

  • UI 状态转换:按點按鈕改變容器大小、颜色等
  • 下拉刷新效果:平滑的容器水平位置变化
  • 扩展/折叠效果:平滑的容器展开和折叠
  • 互动效果:对用户互动做出平滑的视觉反馈

AnimatedContainer 的优势在于它不需要手动管理 AnimationController,仅需要修改属性值即可。在企业应用中,需要处理复杂的动画效果、不同屏幕尺寸的适配、性能优化等问题。

此外,动画还应支持键盘导航、无障碍支持、性能优化等功能。在 PC 端应用中,需要确保动画效果流畅,不影响应用性能。

核心概念

1. AnimatedContainer(动画容器)

AnimatedContainer 是一个隐式动画组件,自动处理属性变化的动画过程。其主要特点包括:

  • 自动动画化:当属性值改变时,自动生成平滑的过渡动画
  • duration 参数:指定动画时长,单位为毫秒。常见值为 300-500ms
  • curve 参数:指定动画曲线,如 Curves.easeInOut、Curves.linear 等
  • onEnd 回调:动画完成时触发,可用于链式动画
  • child 参数:容器内的子组件
  • 性能高效:不需要手动管理 AnimationController,代码简洁

2. 可动画化属性

AnimatedContainer 支持多种属性的动画化:

  • 大小属性:width、height、constraints
  • 颜色属性:color、backgroundColor、foregroundColor
  • 圆角属性:borderRadius、border
  • 内边距:padding、margin
  • 对齐:alignment
  • 其他属性:transform、decoration 等

3. 动画触发与控制

动画的触发和控制方式:

  • setState 触发:修改属性值后调用 setState,自动触发动画
  • 多属性同时动画:多个属性同时改变时,一起动画化
  • 链式动画:通过 onEnd 回调实现多个动画的顺序执行
  • 条件动画:根据不同条件改变属性值,实现不同的动画效果

代码详解

1. 基础大小动画

最简单的 AnimatedContainer 实现,通过改变宽度和高度属性来创建缩放动画。这种方式适合实现展开/折叠效果或响应用户点击的大小变化。

class BasicAnimationWidget extends StatefulWidget {
  
  State<BasicAnimationWidget> createState() => _BasicAnimationWidgetState();
}

class _BasicAnimationWidgetState extends State<BasicAnimationWidget> {
  bool _isExpanded = false;
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        GestureDetector(
          onTap: () => setState(() => _isExpanded = !_isExpanded),
          child: AnimatedContainer(
            duration: Duration(milliseconds: 500),
            width: _isExpanded ? 200 : 100,
            height: _isExpanded ? 200 : 100,
            color: Colors.blue,
            curve: Curves.easeInOut,
            child: Center(
              child: Text(_isExpanded ? '展开' : '折叠'),
            ),
          ),
        ),
      ],
    );
  }
}

这个实现创建了一个可点击的容器,点击时会平滑地改变大小。duration 参数设置动画时长为 500ms,curve 参数使用 easeInOut 曲线使动画更自然。

2. 圆角动画

通过改变 borderRadius 属性,可以实现从方形到圆形的平滑过渡。这种效果常用于按钮、卡片等组件的交互反馈。

AnimatedContainer(
  duration: Duration(milliseconds: 500),
  width: 100,
  height: 100,
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(_isExpanded ? 50 : 8),
  ),
  curve: Curves.easeInOut,
  child: Center(child: Text('圆角动画')),
)

这个实现在圆角和方形之间平滑过渡。当 _isExpanded 为 true 时,borderRadius 为 50(完全圆形),否则为 8(轻微圆角)。

3. 颜色动画

改变容器的颜色属性可以创建平滑的颜色过渡效果。这种效果常用于状态指示、主题切换等场景。

class ColorAnimationWidget extends StatefulWidget {
  
  State<ColorAnimationWidget> createState() => _ColorAnimationWidgetState();
}

class _ColorAnimationWidgetState extends State<ColorAnimationWidget> {
  Color _color = Colors.blue;
  
  void _changeColor() {
    setState(() {
      _color = _color == Colors.blue ? Colors.red : Colors.blue;
    });
  }
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedContainer(
          duration: Duration(milliseconds: 500),
          width: 100,
          height: 100,
          color: _color,
          curve: Curves.easeInOut,
          child: Center(child: Text('颜色动画')),
        ),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: _changeColor,
          child: Text('改变颜色'),
        ),
      ],
    );
  }
}

这个实现展示了如何通过改变 color 属性来创建颜色过渡动画。点击按钮时,容器会平滑地从蓝色变为红色,或反之。

4. 多属性同时动画

最强大的 AnimatedContainer 用法是同时改变多个属性。这样可以创建复杂的动画效果,如卡片展开、菜单弹出等。

class MultiPropertyAnimationWidget extends StatefulWidget {
  
  State<MultiPropertyAnimationWidget> createState() => _MultiPropertyAnimationWidgetState();
}

class _MultiPropertyAnimationWidgetState extends State<MultiPropertyAnimationWidget> {
  bool _isExpanded = false;
  
  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      child: AnimatedContainer(
        duration: Duration(milliseconds: 600),
        width: _isExpanded ? 300 : 100,
        height: _isExpanded ? 300 : 100,
        color: _isExpanded ? Colors.green : Colors.blue,
        borderRadius: BorderRadius.circular(_isExpanded ? 20 : 8),
        padding: EdgeInsets.all(_isExpanded ? 24 : 8),
        curve: Curves.easeInOut,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              _isExpanded ? '展开状态' : '折叠状态',
              style: TextStyle(
                fontSize: _isExpanded ? 18 : 14,
                color: Colors.white,
              ),
            ),
            if (_isExpanded)
              Padding(
                padding: EdgeInsets.only(top: 16),
                child: Text('这是展开后的详细内容'),
              ),
          ],
        ),
      ),
    );
  }
}

这个实现同时改变了宽度、高度、颜色、圆角和内边距等多个属性。所有属性的变化都会在同一个动画中平滑进行,创建了一个完整的卡片展开效果。

5. 链式动画

通过 onEnd 回调,可以在一个动画完成后立即启动另一个动画,实现复杂的动画序列。

class ChainedAnimationWidget extends StatefulWidget {
  
  State<ChainedAnimationWidget> createState() => _ChainedAnimationWidgetState();
}

class _ChainedAnimationWidgetState extends State<ChainedAnimationWidget> {
  double _width = 100;
  double _height = 100;
  Color _color = Colors.blue;
  
  void _startChainedAnimation() {
    setState(() {
      _width = 200;
      _height = 100;
    });
  }
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedContainer(
          duration: Duration(milliseconds: 500),
          width: _width,
          height: _height,
          color: _color,
          curve: Curves.easeInOut,
          onEnd: () {
            if (_width == 200 && _height == 100) {
              setState(() {
                _height = 200;
              });
            }
          },
          child: Center(child: Text('链式动画')),
        ),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: _startChainedAnimation,
          child: Text('开始链式动画'),
        ),
      ],
    );
  }
}

这个实现展示了如何使用 onEnd 回调来实现链式动画。首先改变宽度,当动画完成时,onEnd 回调会改变高度,从而实现两个动画的顺序执行。

高级话题:AnimatedContainer 的企业级应用

1. 动态/响应式设计与多屏幕适配

在企业应用中,动画需要在不同屏幕尺寸下提供一致的效果。根据屏幕宽度调整动画的目标值,确保在所有设备上都有合适的动画效果。

class ResponsiveAnimationWidget extends StatefulWidget {
  
  State<ResponsiveAnimationWidget> createState() => _ResponsiveAnimationWidgetState();
}

class _ResponsiveAnimationWidgetState extends State<ResponsiveAnimationWidget> {
  bool _isExpanded = false;
  
  
  Widget build(BuildContext context) {
    final screenWidth = MediaQuery.of(context).size.width;
    final isMobile = screenWidth < 600;
    
    final targetWidth = _isExpanded ? (isMobile ? 200 : 400) : 100;
    final targetHeight = _isExpanded ? (isMobile ? 200 : 300) : 100;
    
    return GestureDetector(
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      child: AnimatedContainer(
        duration: Duration(milliseconds: 500),
        width: targetWidth,
        height: targetHeight,
        color: Colors.blue,
        curve: Curves.easeInOut,
        child: Center(child: Text('响应式动画')),
      ),
    );
  }
}

这个实现根据屏幕宽度动态调整动画的目标值,确保在移动设备和 PC 端都有合适的动画效果。

2. 动画曲线与缓动效果

不同的动画曲线会产生不同的视觉效果。选择合适的曲线可以使动画看起来更自然、更符合应用的风格。

class CurveAnimationWidget extends StatefulWidget {
  
  State<CurveAnimationWidget> createState() => _CurveAnimationWidgetState();
}

class _CurveAnimationWidgetState extends State<CurveAnimationWidget> {
  bool _isExpanded = false;
  String _selectedCurve = 'easeInOut';
  
  Curve _getCurve(String curveName) {
    switch (curveName) {
      case 'linear':
        return Curves.linear;
      case 'easeIn':
        return Curves.easeIn;
      case 'easeOut':
        return Curves.easeOut;
      case 'easeInOut':
        return Curves.easeInOut;
      case 'elasticOut':
        return Curves.elasticOut;
      default:
        return Curves.easeInOut;
    }
  }
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedContainer(
          duration: Duration(milliseconds: 800),
          width: _isExpanded ? 200 : 100,
          height: _isExpanded ? 200 : 100,
          color: Colors.blue,
          curve: _getCurve(_selectedCurve),
          child: Center(child: Text('曲线: $_selectedCurve')),
        ),
        SizedBox(height: 16),
        Wrap(
          spacing: 8,
          children: ['linear', 'easeIn', 'easeOut', 'easeInOut', 'elasticOut']
              .map((curve) => ElevatedButton(
                    onPressed: () => setState(() => _selectedCurve = curve),
                    child: Text(curve),
                  ))
              .toList(),
        ),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: () => setState(() => _isExpanded = !_isExpanded),
          child: Text('开始动画'),
        ),
      ],
    );
  }
}

这个实现展示了不同动画曲线的效果。用户可以选择不同的曲线来体验不同的动画效果。

3. 搜索/过滤/排序功能

在列表中使用 AnimatedContainer 可以创建平滑的过滤和排序效果。

class FilteredAnimationListWidget extends StatefulWidget {
  
  State<FilteredAnimationListWidget> createState() => _FilteredAnimationListWidgetState();
}

class _FilteredAnimationListWidgetState extends State<FilteredAnimationListWidget> {
  final List<String> _items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
  final Set<int> _expandedItems = {};
  
  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: _items.length,
      itemBuilder: (context, index) {
        final isExpanded = _expandedItems.contains(index);
        
        return GestureDetector(
          onTap: () => setState(() {
            if (isExpanded) {
              _expandedItems.remove(index);
            } else {
              _expandedItems.add(index);
            }
          }),
          child: AnimatedContainer(
            duration: Duration(milliseconds: 300),
            height: isExpanded ? 150 : 60,
            margin: EdgeInsets.all(8),
            padding: EdgeInsets.all(16),
            decoration: BoxDecoration(
              color: Colors.blue.shade100,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(_items[index], style: TextStyle(fontWeight: FontWeight.bold)),
                if (isExpanded)
                  Padding(
                    padding: EdgeInsets.only(top: 16),
                    child: Text('这是 ${_items[index]} 的详细内容'),
                  ),
              ],
            ),
          ),
        );
      },
    );
  }
}

4. 选择与批量操作

使用 AnimatedContainer 可以为选择操作提供视觉反馈。

class SelectableAnimationWidget extends StatefulWidget {
  
  State<SelectableAnimationWidget> createState() => _SelectableAnimationWidgetState();
}

class _SelectableAnimationWidgetState extends State<SelectableAnimationWidget> {
  final Set<int> _selectedItems = {};
  final List<String> _items = List.generate(5, (i) => 'Item ${i + 1}');
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: List.generate(_items.length, (index) {
            final isSelected = _selectedItems.contains(index);
            
            return GestureDetector(
              onTap: () => setState(() {
                if (isSelected) {
                  _selectedItems.remove(index);
                } else {
                  _selectedItems.add(index);
                }
              }),
              child: AnimatedContainer(
                duration: Duration(milliseconds: 300),
                padding: EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: isSelected ? Colors.blue : Colors.grey.shade300,
                  borderRadius: BorderRadius.circular(8),
                  border: Border.all(
                    color: isSelected ? Colors.blue : Colors.transparent,
                    width: 2,
                  ),
                ),
                child: Text(
                  _items[index],
                  style: TextStyle(
                    color: isSelected ? Colors.white : Colors.black,
                  ),
                ),
              ),
            );
          }),
        ),
        SizedBox(height: 16),
        Text('已选择 ${_selectedItems.length} 项'),
      ],
    );
  }
}

5. 加载与缓存策略

使用 AnimatedContainer 可以创建平滑的加载状态指示。

class LoadingAnimationWidget extends StatefulWidget {
  
  State<LoadingAnimationWidget> createState() => _LoadingAnimationWidgetState();
}

class _LoadingAnimationWidgetState extends State<LoadingAnimationWidget> {
  bool _isLoading = false;
  
  void _startLoading() async {
    setState(() => _isLoading = true);
    await Future.delayed(Duration(seconds: 2));
    setState(() => _isLoading = false);
  }
  
  
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedContainer(
          duration: Duration(milliseconds: 500),
          width: _isLoading ? 200 : 100,
          height: _isLoading ? 200 : 100,
          decoration: BoxDecoration(
            color: _isLoading ? Colors.blue : Colors.grey,
            borderRadius: BorderRadius.circular(_isLoading ? 50 : 8),
          ),
          child: Center(
            child: _isLoading
                ? CircularProgressIndicator(color: Colors.white)
                : Text('点击加载'),
          ),
        ),
        SizedBox(height: 16),
        ElevatedButton(
          onPressed: _startLoading,
          child: Text('开始加载'),
        ),
      ],
    );
  }
}

6. 键盘导航与快捷键

为 AnimatedContainer 添加键盘支持,提高 PC 端的可用性。

class KeyboardAnimationWidget extends StatefulWidget {
  
  State<KeyboardAnimationWidget> createState() => _KeyboardAnimationWidgetState();
}

class _KeyboardAnimationWidgetState extends State<KeyboardAnimationWidget> {
  bool _isExpanded = false;
  late FocusNode _focusNode;
  
  
  void initState() {
    super.initState();
    _focusNode = FocusNode();
  }
  
  
  void dispose() {
    _focusNode.dispose();
    super.dispose();
  }
  
  void _handleKeyEvent(RawKeyEvent event) {
    if (event.isKeyPressed(LogicalKeyboardKey.space) ||
        event.isKeyPressed(LogicalKeyboardKey.enter)) {
      setState(() => _isExpanded = !_isExpanded);
    }
  }
  
  
  Widget build(BuildContext context) {
    return RawKeyboardListener(
      focusNode: _focusNode,
      onKey: _handleKeyEvent,
      child: AnimatedContainer(
        duration: Duration(milliseconds: 500),
        width: _isExpanded ? 200 : 100,
        height: _isExpanded ? 200 : 100,
        color: Colors.blue,
        child: Center(child: Text('按 Space 或 Enter 切换')),
      ),
    );
  }
}

7. 无障碍支持与屏幕阅读器

为动画添加无障碍标签,确保屏幕阅读器用户也能理解动画状态。

class AccessibleAnimationWidget extends StatefulWidget {
  
  State<AccessibleAnimationWidget> createState() => _AccessibleAnimationWidgetState();
}

class _AccessibleAnimationWidgetState extends State<AccessibleAnimationWidget> {
  bool _isExpanded = false;
  
  
  Widget build(BuildContext context) {
    return Semantics(
      label: '动画容器,当前状态${_isExpanded ? "展开" : "折叠"}',
      button: true,
      enabled: true,
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      child: GestureDetector(
        onTap: () => setState(() => _isExpanded = !_isExpanded),
        child: AnimatedContainer(
          duration: Duration(milliseconds: 500),
          width: _isExpanded ? 200 : 100,
          height: _isExpanded ? 200 : 100,
          color: Colors.blue,
          child: Center(
            child: Semantics(
              label: _isExpanded ? '展开状态' : '折叠状态',
              child: Text(_isExpanded ? '展开' : '折叠'),
            ),
          ),
        ),
      ),
    );
  }
}

8. 样式自定义与主题适配

创建可配置的动画主题,支持不同的设计风格。

class AnimationTheme {
  final Duration duration;
  final Curve curve;
  final Color expandedColor;
  final Color collapsedColor;
  
  const AnimationTheme({
    this.duration = const Duration(milliseconds: 500),
    this.curve = Curves.easeInOut,
    this.expandedColor = Colors.blue,
    this.collapsedColor = Colors.grey,
  });
  
  static AnimationTheme light() {
    return AnimationTheme(
      expandedColor: Colors.blue,
      collapsedColor: Colors.grey.shade300,
    );
  }
  
  static AnimationTheme dark() {
    return AnimationTheme(
      expandedColor: Colors.blue.shade300,
      collapsedColor: Colors.grey.shade700,
    );
  }
}

class ThemedAnimationWidget extends StatefulWidget {
  final AnimationTheme theme;
  
  const ThemedAnimationWidget({required this.theme});
  
  
  State<ThemedAnimationWidget> createState() => _ThemedAnimationWidgetState();
}

class _ThemedAnimationWidgetState extends State<ThemedAnimationWidget> {
  bool _isExpanded = false;
  
  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      child: AnimatedContainer(
        duration: widget.theme.duration,
        curve: widget.theme.curve,
        width: _isExpanded ? 200 : 100,
        height: _isExpanded ? 200 : 100,
        color: _isExpanded ? widget.theme.expandedColor : widget.theme.collapsedColor,
        child: Center(child: Text('主题动画')),
      ),
    );
  }
}

9. 数据持久化与导出

保存动画状态以便后续恢复。

class PersistentAnimationWidget extends StatefulWidget {
  
  State<PersistentAnimationWidget> createState() => _PersistentAnimationWidgetState();
}

class _PersistentAnimationWidgetState extends State<PersistentAnimationWidget> {
  bool _isExpanded = false;
  
  
  void initState() {
    super.initState();
    _loadAnimationState();
  }
  
  Future<void> _loadAnimationState() async {
    // 从本地存储加载动画状态
    // final prefs = await SharedPreferences.getInstance();
    // _isExpanded = prefs.getBool('isExpanded') ?? false;
  }
  
  void _saveAnimationState() async {
    // 保存动画状态到本地存储
    // final prefs = await SharedPreferences.getInstance();
    // await prefs.setBool('isExpanded', _isExpanded);
  }
  
  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() => _isExpanded = !_isExpanded);
        _saveAnimationState();
      },
      child: AnimatedContainer(
        duration: Duration(milliseconds: 500),
        width: _isExpanded ? 200 : 100,
        height: _isExpanded ? 200 : 100,
        color: Colors.blue,
        child: Center(child: Text('持久化动画')),
      ),
    );
  }
}

10. 单元测试与集成测试

为动画编写测试用例,确保动画功能的正确性。

void main() {
  group('AnimatedContainer Tests', () {
    test('容器大小变化', () {
      double width = 100;
      expect(width, 100);
      width = 200;
      expect(width, 200);
    });
    
    test('颜色变化', () {
      Color color = Colors.blue;
      expect(color, Colors.blue);
      color = Colors.red;
      expect(color, Colors.red);
    });
  });
  
  testWidgets('AnimatedContainer 集成测试', (WidgetTester tester) async {
    bool isExpanded = false;
    
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: StatefulBuilder(
            builder: (context, setState) {
              return GestureDetector(
                onTap: () => setState(() => isExpanded = !isExpanded),
                child: AnimatedContainer(
                  duration: Duration(milliseconds: 500),
                  width: isExpanded ? 200 : 100,
                  height: isExpanded ? 200 : 100,
                  color: Colors.blue,
                ),
              );
            },
          ),
        ),
      ),
    );
    
    expect(find.byType(AnimatedContainer), findsOneWidget);
    
    await tester.tap(find.byType(GestureDetector));
    await tester.pumpAndSettle();
  });
}

OpenHarmony PC 端适配要点

  1. 屏幕宽度检测:根据不同屏幕宽度调整动画的目标值
  2. 响应式动画:在 PC 端使用更大的动画范围
  3. 键盘导航:支持 Space、Enter 等快捷键触发动画
  4. 鼠标交互:支持悬停效果和点击反馈
  5. 性能优化:避免频繁的 setState 调用

实际应用场景

  1. UI 状态转换:按钮点击改变容器状态
  2. 下拉刷新:平滑的刷新指示器
  3. 扩展/折叠:菜单、卡片的展开效果
  4. 加载状态:平滑的加载动画
  5. 主题切换:平滑的颜色过渡

扩展建议

  1. 支持多个动画的并行执行
  2. 实现更复杂的动画序列
  3. 添加动画的暂停和恢复
  4. 支持动画的反向播放
  5. 实现自定义动画曲线

总结

AnimatedContainer 是创建平滑动画效果的最简单方式。通过合理的设计和实现,可以创建出功能完整、用户体验良好的动画系统。在 PC 端应用中,充分利用屏幕空间、提供键盘导航和无障碍支持是关键。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐