AnimatedContainer动画容器 Flutter & OpenHarmony
本案例介绍了Flutter中AnimatedContainer组件的使用方法和应用场景。AnimatedContainer是一个隐式动画组件,能够自动处理大小、颜色、圆角等属性变化的动画过渡,无需手动管理AnimationController。文章详细讲解了AnimatedContainer的核心概念,包括其自动动画化特性、可动画化的多种属性(如大小、颜色、圆角等)以及动画触发控制方式。通过代码示

案例概述
本案例展示如何使用 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 端适配要点
- 屏幕宽度检测:根据不同屏幕宽度调整动画的目标值
- 响应式动画:在 PC 端使用更大的动画范围
- 键盘导航:支持 Space、Enter 等快捷键触发动画
- 鼠标交互:支持悬停效果和点击反馈
- 性能优化:避免频繁的 setState 调用
实际应用场景
- UI 状态转换:按钮点击改变容器状态
- 下拉刷新:平滑的刷新指示器
- 扩展/折叠:菜单、卡片的展开效果
- 加载状态:平滑的加载动画
- 主题切换:平滑的颜色过渡
扩展建议
- 支持多个动画的并行执行
- 实现更复杂的动画序列
- 添加动画的暂停和恢复
- 支持动画的反向播放
- 实现自定义动画曲线
总结
AnimatedContainer 是创建平滑动画效果的最简单方式。通过合理的设计和实现,可以创建出功能完整、用户体验良好的动画系统。在 PC 端应用中,充分利用屏幕空间、提供键盘导航和无障碍支持是关键。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)