鸿蒙跨端Flutter学习——GridView动画效果
GridView动画效果

23-08 GridView动画效果
知识点概述
动画是提升用户体验的重要手段。在GridView中应用适当的动画效果,可以让界面更加生动,提供清晰的交互反馈,引导用户注意力。本章将详细介绍各种GridView动画技术,包括进入动画、交错动画、过渡动画、交互动画等,帮助您为GridView添加流畅、优雅的动画效果。
GridView动画的核心价值:
| 价值维度 | 具体表现 | 用户体验影响 | 技术实现 |
|---|---|---|---|
| 视觉吸引力 | 平滑过渡、自然运动 | 提升应用品质感 | 动画曲线、缓动函数 |
| 交互反馈 | 即时响应、明确反馈 | 增强操作确定性 | 点击动画、状态动画 |
| 引导注意力 | 突出重点、流程引导 | 帮助用户理解 | 视差效果、Hero动画 |
| 情感表达 | 活泼、优雅、专业 | 建立品牌印象 | 动画风格统一化 |
动画性能目标:
1. 进入动画实现
进入动画是指GridView中的item首次显示时的动画效果。一个好的进入动画可以让界面加载过程更加平滑自然,给用户留下深刻的第一印象。
1.1 基础淡入动画
淡入动画是最简单也最常用的进入效果。通过Opacity组件配合AnimationController,可以让item从透明逐渐变为不透明,实现平滑的显示效果。
实现要点:
- 使用AnimationController控制动画进度
- 通过Tween定义0.0到1.0的透明度变化
- 选择合适的CurvedAnimation曲线增强自然感
class FadeInGridView extends StatefulWidget {
_FadeInGridViewState createState() => _FadeInGridViewState();
}
class _FadeInGridViewState extends State<FadeInGridView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 600),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_controller.forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('淡入动画')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemCount: 20,
itemBuilder: (context, index) {
return FadeTransition(
opacity: _animation,
child: Card(
child: Center(
child: Text(
'Item $index',
style: TextStyle(fontSize: 20),
),
),
),
);
},
),
);
}
}
1.2 缩放进入动画
缩放动画通过改变item的大小来实现进入效果。从0倍缩放到1倍,或者从略大于1倍缩放到正常大小,可以创造出弹性的视觉冲击力。
适用场景:
- 卡片类内容展示
- 图片网格
- 需要强调出现的元素
| 缩放类型 | 起始比例 | 曲线特点 | 视觉效果 | 推荐场景 |
|---|---|---|---|---|
| 小变大 | 0.0 → 1.0 | easeOut | 弹跳感强 | 强调显示 |
| 大变小 | 1.2 → 1.0 | elastic | 弹性效果 | 图片展示 |
| 脉冲式 | 0.8 → 1.0 → 0.95 → 1.0 | 自定义 | 节奏感 | 重要提示 |
class ScaleInGridView extends StatefulWidget {
_ScaleInGridViewState createState() => _ScaleInGridViewState();
}
class _ScaleInGridViewState extends State<ScaleInGridView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
);
_controller.forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('缩放进入')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemCount: 18,
itemBuilder: (context, index) {
return ScaleTransition(
scale: _animation,
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
),
),
);
},
),
);
}
}
1.3 滑动进入动画
滑动动画让item从屏幕外滑入,创造空间感的进入效果。可以从不同方向滑入,配合不同的曲线,产生丰富的视觉变化。
滑动方向对比:
2. 交错动画效果
交错动画让多个item按一定顺序依次执行动画,而不是同时执行。这样可以创造出层次感和节奏感,让界面加载过程更加优雅。
2.1 基于索引的交错动画
通过为每个item设置不同的动画延迟,可以实现从左到右、从上到下等不同方向的交错效果。延迟时间通常与item的索引成正比。
延迟计算公式:
delay i = baseDelay + i × staggerDelay \text{delay}_i = \text{baseDelay} + i \times \text{staggerDelay} delayi=baseDelay+i×staggerDelay
其中:
- ( \text{delay}_i ): 第i个item的延迟时间
- ( \text{baseDelay} ): 基础延迟时间
- ( i ): item的索引
- ( \text{staggerDelay} ): 交错间隔时间
| 交错模式 | 延迟计算 | 效果特点 | 适用场景 |
|---|---|---|---|
| 从左到右 | baseDelay + i × interval | 横向流动感 | 横向排列的网格 |
| 从上到下 | baseDelay + row × interval | 纵向流动感 | 垂直滚动列表 |
| 中心向外 | baseDelay + distance × interval | 扩散效果 | 重要内容展示 |
| 随机交错 | baseDelay + random × interval | 自然感强 | 瀑布流布局 |
class StaggeredGridView extends StatefulWidget {
_StaggeredGridViewState createState() => _StaggeredGridViewState();
}
class _StaggeredGridViewState extends State<StaggeredGridView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final int _itemCount = 24;
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_controller.forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('交错动画')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: _itemCount,
itemBuilder: (context, index) {
// 为每个item创建独立的动画
final animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
index * 0.05, // 开始时间基于索引
(index * 0.05) + 0.5, // 每个动画持续0.5的比例
curve: Curves.easeOut,
),
),
);
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.primaries[index % Colors.primaries.length],
),
child: Center(
child: Text(
'$index',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
},
),
);
}
}
2.2 复合交错动画
复合交错动画结合多种动画效果,如淡入+缩放+位移,创造出更丰富的视觉表现。每个动画可以有自己的时间曲线和延迟策略。
动画组合策略:
3. 过渡动画效果
过渡动画是指GridView中数据变化时的动画效果,如插入、删除、更新item时的动画。良好的过渡动画可以让用户清楚地感知数据的变化。
3.1 AnimatedList与GridView的集成
虽然AnimatedList专为列表设计,但通过CustomScrollView和SliverGrid,可以在Grid布局中实现类似的过渡动画效果。
实现原理:
- 使用AnimatedList作为基础组件
- 将每个列表项包装为Grid item
- 通过Grid布局实现多列显示
- 利用AnimatedList的内置过渡动画
class AnimatedGridView extends StatefulWidget {
_AnimatedGridViewState createState() => _AnimatedGridViewState();
}
class _AnimatedGridViewState extends State<AnimatedGridView> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
final List<String> _items = List.generate(10, (index) => 'Item $index');
void _addItem() {
final newIndex = _items.length;
_items.add('Item $newIndex');
_listKey.currentState?.insertItem(newIndex);
}
void _removeItem(int index) {
_listKey.currentState?.removeItem(
index,
(context, animation) => _buildItem(_items[index], animation),
);
_items.removeAt(index);
}
Widget _buildItem(String item, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
child: Card(
margin: EdgeInsets.all(4),
child: ListTile(
title: Text(item),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {},
),
),
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('过渡动画'),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: _addItem,
),
],
),
body: AnimatedList(
key: _listKey,
initialItemCount: _items.length,
itemBuilder: (context, index, animation) {
return _buildItem(_items[index], animation);
},
),
);
}
}
3.2 手动过渡动画实现
对于更复杂的Grid布局,可以手动实现过渡动画。通过维护动画列表,在数据变化时触发相应的动画。
动画状态管理:
| 操作类型 | 动画效果 | 持续时间 | 触发时机 | 清理时机 |
|---|---|---|---|---|
| 插入 | 淡入+缩放 | 300ms | 数据添加后 | 动画完成后 |
| 删除 | 淡出+缩小 | 300ms | 数据移除前 | 动画完成后 |
| 移动 | 位移动画 | 200ms | 位置改变时 | 动画完成后 |
| 更新 | 闪烁+颜色变化 | 150ms | 内容变化时 | 立即完成 |
class CustomTransitionGrid extends StatefulWidget {
_CustomTransitionGridState createState() => _CustomTransitionGridState();
}
class _CustomTransitionGridState extends State<CustomTransitionGrid> {
final List<String> _items = List.generate(15, (index) => 'Item $index');
final Map<int, AnimationController> _controllers = {};
void dispose() {
_controllers.forEach((key, controller) => controller.dispose());
super.dispose();
}
Widget _buildItem(int index) {
return Card(
key: ValueKey(index),
elevation: 3,
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
_items[index],
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('手动过渡')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: _items.length,
itemBuilder: (context, index) {
return AnimatedSize(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: _buildItem(index),
);
},
),
);
}
}
4. 交互动画效果
交互动画响应用户的触摸、点击等操作,提供即时的视觉反馈,增强交互的确定性和愉悦感。
4.1 点击反馈动画
点击动画是最基础的交互动画,包括缩放、颜色变化、波纹效果等。这些动画应该在用户点击时立即触发,持续时间通常在150-300ms之间。
点击反馈类型:
| 反馈类型 | 视觉效果 | 实现方式 | 适用场景 | 持续时间 |
|---|---|---|---|---|
| 缩放 | 按下缩小,释放复原 | ScaleTransition | 按钮、卡片 | 150-200ms |
| 波纹 | 水波纹扩散 | InkWell | 列表项 | 300-400ms |
| 颜色 | 颜色加深/变化 | ColorTween | 重要操作 | 200ms |
| 透明度 | 暂时半透明 | Opacity | 可点击元素 | 150ms |
class ClickFeedbackGrid extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('点击反馈')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: 12,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('点击了 Item $index')),
);
},
splashColor: Colors.blue.withOpacity(0.5),
highlightColor: Colors.blue.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
child: Container(
margin: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'Item $index',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
);
},
),
);
}
}
4.2 长按交互动画
长按动画通常用于触发上下文菜单或进入编辑模式。长按时可以显示视觉反馈,如边框变化、图标出现等。
class LongPressGrid extends StatefulWidget {
_LongPressGridState createState() => _LongPressGridState();
}
class _LongPressGridState extends State<LongPressGrid> {
final Set<int> _selectedItems = {};
bool _isSelectionMode = false;
void _toggleSelection(int index) {
setState(() {
if (_selectedItems.contains(index)) {
_selectedItems.remove(index);
} else {
_selectedItems.add(index);
}
if (_selectedItems.isEmpty) {
_isSelectionMode = false;
}
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isSelectionMode
? '已选 ${_selectedItems.length} 项'
: '长按交互'),
actions: _isSelectionMode
? [
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
// 处理删除
},
),
]
: null,
),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemCount: 15,
itemBuilder: (context, index) {
final isSelected = _selectedItems.contains(index);
return GestureDetector(
onTap: () {
if (_isSelectionMode) {
_toggleSelection(index);
}
},
onLongPress: () {
setState(() {
_isSelectionMode = true;
_selectedItems.add(index);
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
margin: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
border: isSelected
? Border.all(color: Colors.white, width: 3)
: null,
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
spreadRadius: 2,
),
]
: null,
),
child: Stack(
children: [
Center(
child: Text(
'$index',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
if (isSelected)
Positioned(
top: 4,
right: 4,
child: Icon(
Icons.check_circle,
color: Colors.white,
),
),
],
),
),
);
},
),
);
}
}
5. 滚动动画效果
滚动动画与GridView的滚动行为相关,可以在滚动过程中创造视差效果、透明度变化、缩放效果等,增强滚动体验。
5.1 视差滚动效果
视差效果是指不同元素以不同速度滚动,创造出深度感和层次感。可以通过监听滚动位置,根据位置计算不同的偏移量来实现。
视差效果层级:
class ParallaxGrid extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('视差滚动')),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.blue,
child: Center(
child: Text(
'Header',
style: TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
),
),
),
SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
return Card(
elevation: 2,
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'$index',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
);
},
childCount: 30,
),
),
),
],
),
);
}
}
5.2 滚动触发动画
当item进入或离开可视区域时触发动画,可以创造动态的滚动体验。这需要监听滚动位置,计算item的可见性。
| 可见性状态 | 动画类型 | 触发条件 | 效果描述 |
|---|---|---|---|
| 进入视口 | 淡入 | item顶部进入 | 逐渐显示 |
| 完全可见 | 无变化 | 完全在视口内 | 保持状态 |
| 离开视口 | 淡出 | item底部离开 | 逐渐消失 |
| 部分可见 | 部分效果 | 边缘位置 | 倾斜或缩放 |
class ScrollTriggeredGrid extends StatefulWidget {
_ScrollTriggeredGridState createState() => _ScrollTriggeredGridState();
}
class _ScrollTriggeredGridState extends State<ScrollTriggeredGrid> {
final ScrollController _controller = ScrollController();
final Map<int, AnimationController> _itemControllers = {};
final Map<int, Animation<double>> _itemAnimations = {};
void initState() {
super.initState();
_controller.addListener(_onScroll);
}
void dispose() {
_controller.dispose();
_itemControllers.forEach((_, controller) => controller.dispose());
super.dispose();
}
void _onScroll() {
// 计算每个item的可见性并触发动画
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('滚动触发')),
body: GridView.builder(
controller: _controller,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemCount: 50,
itemBuilder: (context, index) {
return Card(
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'$index',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
);
},
),
);
}
}
6. Hero动画效果
Hero动画让一个元素从页面A"飞"到页面B,创造流畅的导航体验。在GridView中,Hero动画可以用于从item过渡到详情页。
6.1 基础Hero动画
通过为GridView中的item和详情页的内容设置相同的heroTag,Flutter会自动处理它们之间的过渡动画。
Hero动画流程:
class HeroGrid extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Hero动画')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 20,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(index: index),
),
);
},
child: Hero(
tag: 'hero_$index',
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
child: Center(
child: Text(
'Item $index',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
},
),
);
}
}
class DetailPage extends StatelessWidget {
final int index;
DetailPage({required this.index});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('详情 $index')),
body: Center(
child: Hero(
tag: 'hero_$index',
child: Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
child: Center(
child: Text(
'Detail $index',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
);
}
}
6.2 自定义Hero动画
通过自定义Hero的flightShuttleBuilder和placeholderBuilder,可以实现更丰富的过渡效果,如形状变化、颜色渐变等。
class CustomHeroGrid extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('自定义Hero')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: 10,
itemBuilder: (context, index) {
return Hero(
tag: 'hero_$index',
flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
return DefaultTextStyle(
style: DefaultTextStyle.of(toHeroContext).style,
child: toHeroContext.widget,
);
},
child: Card(
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'Item $index',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
),
);
},
),
);
}
}
7. 动画性能优化
动画虽然能提升用户体验,但如果不注意性能优化,反而会导致卡顿和流畅度下降。掌握动画优化技巧至关重要。
7.1 避免过度动画
不是所有元素都需要动画,过多的动画会增加渲染负担。应该有选择地为关键元素添加动画效果。
动画优先级:
| 优先级 | 动画类型 | 原因 | 示例 |
|---|---|---|---|
| 高 | 进入动画 | 第一印象重要 | 页面首次加载 |
| 中 | 交互动画 | 反馈必要性 | 点击、长按 |
| 低 | 装饰动画 | 可选增强 | 背景、图标 |
| 无 | 静态元素 | 无动画必要 | 文本内容 |
7.2 使用硬件加速
Flutter默认使用硬件加速,但某些操作会强制使用CPU渲染。确保动画操作在GPU层执行,可以显著提升性能。
硬件加速检查清单:
- ✅ 使用Opacity而不是Container(color: withOpacity)
- ✅ 使用Transform而不是手动计算位置
- ✅ 使用BackdropFilter实现模糊效果
- ✅ 避免复杂的Clip路径动画
- ❌ 避免频繁调用setState触发重绘
- ❌ 避免动画过程中执行复杂计算
class OptimizedAnimationGrid extends StatefulWidget {
_OptimizedAnimationGridState createState() => _OptimizedAnimationGridState();
}
class _OptimizedAnimationGridState extends State<OptimizedAnimationGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('优化动画')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemCount: 30,
itemBuilder: (context, index) {
return Transform.scale(
scale: _animation.value,
child: Opacity(
opacity: _animation.value,
child: Card(
elevation: 2,
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'$index',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
),
);
},
),
);
}
}
7.3 动画资源管理
动画控制器和动画对象需要及时释放,否则会造成内存泄漏。使用StateMixin或自动管理机制确保资源正确释放。
资源管理最佳实践:
class ResourceManagedGrid extends StatefulWidget {
_ResourceManagedGridState createState() => _ResourceManagedGridState();
}
class _ResourceManagedGridState extends State<ResourceManagedGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
bool _isDisposed = false;
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
)..addStatusListener((status) {
if (status == AnimationStatus.completed && !_isDisposed) {
// 动画完成后的处理
}
});
_controller.forward();
}
void dispose() {
_isDisposed = true;
_controller.dispose(); // 必须释放
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('资源管理')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: 20,
itemBuilder: (context, index) {
return Card(
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'$index',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
);
},
),
);
}
}
8. 综合实战案例
将前面学到的动画技术综合应用,创建一个功能完整、效果丰富的GridView动画示例。
8.1 综合动画Grid实现
这个示例整合了进入动画、交错效果、点击反馈、Hero动画等多种技术。
动画特性:
- 🎬 进入时采用缩放+淡入的交错动画
- 👆 点击时有水波纹反馈和缩放效果
- 🚀 Hero动画过渡到详情页
- 📊 支持长按选择模式
- 💫 滚动时有性能优化
class ComprehensiveAnimationGrid extends StatefulWidget {
_ComprehensiveAnimationGridState createState() =>
_ComprehensiveAnimationGridState();
}
class _ComprehensiveAnimationGridState
extends State<ComprehensiveAnimationGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final int _itemCount = 24;
final Set<int> _selectedItems = {};
bool _isSelectionMode = false;
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1200),
vsync: this,
)..forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleLongPress(int index) {
setState(() {
_isSelectionMode = true;
_selectedItems.add(index);
});
}
void _handleTap(int index) {
if (_isSelectionMode) {
setState(() {
if (_selectedItems.contains(index)) {
_selectedItems.remove(index);
} else {
_selectedItems.add(index);
}
if (_selectedItems.isEmpty) {
_isSelectionMode = false;
}
});
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(index: index),
),
);
}
}
Widget _buildItem(int index) {
final animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
(index % 4) * 0.05 + (index ~/ 4) * 0.1,
0.5 + (index % 4) * 0.05 + (index ~/ 4) * 0.1,
curve: Curves.easeOutBack,
),
),
);
final isSelected = _selectedItems.contains(index);
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: GestureDetector(
onTap: () => _handleTap(index),
onLongPress: () => _handleLongPress(index),
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
margin: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
border: isSelected
? Border.all(color: Colors.white, width: 4)
: null,
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 15,
spreadRadius: 3,
),
]
: null,
),
child: Stack(
children: [
Center(
child: Text(
'$index',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
if (isSelected)
Positioned(
top: 8,
right: 8,
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: Icon(
Icons.check,
color: Colors.green,
size: 20,
),
),
),
],
),
),
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
_isSelectionMode
? '已选 ${_selectedItems.length} 项'
: '综合动画Grid',
),
actions: [
if (_isSelectionMode)
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
_isSelectionMode = false;
_selectedItems.clear();
});
},
),
],
),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: _itemCount,
itemBuilder: (context, index) => _buildItem(index),
),
);
}
}
8.2 动画效果总结
通过这个综合案例,我们学习了:
| 动画类型 | 实现方式 | 难度 | 效果 |
|---|---|---|---|
| 进入动画 | AnimationController + Tween | 中等 | ★★★★★ |
| 交错效果 | Interval延迟 | 简单 | ★★★★ |
| 点击反馈 | InkWell + AnimatedContainer | 简单 | ★★★ |
| 选择模式 | 状态管理 + AnimatedContainer | 中等 | ★★★★ |
| Hero过渡 | Hero Widget | 简单 | ★★★★★ |
| 性能优化 | Transform + Opacity | 中等 | ★★★★ |
知识点总结
本文详细介绍了GridView中各种动画效果的实现方法,从基础的进入动画到复杂的综合案例。掌握这些动画技术,可以显著提升应用的视觉品质和用户体验。
核心要点回顾:
- 进入动画包括淡入、缩放、滑动等多种形式,是GridView动画的基础
- 交错动画通过延迟控制创造层次感,让界面加载更加优雅
- 过渡动画处理数据变化时的动画,让用户清楚感知数据变化
- 交互动画响应用户操作,提供即时反馈,增强交互确定性
- 滚动动画创造视差和动态效果,提升滚动体验
- Hero动画实现页面间的平滑过渡,是Flutter的特色功能
- 性能优化确保动画流畅,避免卡顿,提升用户体验
- 综合应用多种动画技术的组合,创造丰富的交互体验
通过合理应用这些动画技术,可以让GridView从简单的数据展示转变为生动、流畅、富有表现力的交互界面。
9. 完整可运行代码示例
9.1 主应用入口
import 'package:flutter/material.dart';
void main() {
runApp(const GridViewAnimationsApp());
}
class GridViewAnimationsApp extends StatelessWidget {
const GridViewAnimationsApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'GridView动画效果',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GridView动画效果演示'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
_buildDemoCard(
context,
'淡入动画',
Icons.fade,
Colors.blue,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const FadeInGridView(),
),
),
),
const SizedBox(height: 12),
_buildDemoCard(
context,
'缩放进入',
Icons.zoom_in,
Colors.green,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ScaleInGridView(),
),
),
),
const SizedBox(height: 12),
_buildDemoCard(
context,
'交错动画',
Icons.animation,
Colors.orange,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const StaggeredGridView(),
),
),
),
const SizedBox(height: 12),
_buildDemoCard(
context,
'点击反馈',
Icons.touch_app,
Colors.purple,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ClickFeedbackGrid(),
),
),
),
const SizedBox(height: 12),
_buildDemoCard(
context,
'长按选择',
Icons.check_circle,
Colors.red,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LongPressGrid(),
),
),
),
const SizedBox(height: 12),
_buildDemoCard(
context,
'Hero动画',
Icons.flight_takeoff,
Colors.teal,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const HeroGrid(),
),
),
),
const SizedBox(height: 12),
_buildDemoCard(
context,
'综合动画',
Icons.auto_awesome,
Colors.indigo,
() => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ComprehensiveAnimationGrid(),
),
),
),
],
),
);
}
Widget _buildDemoCard(
BuildContext context,
String title,
IconData icon,
Color color,
VoidCallback onTap,
) {
return Card(
elevation: 2,
child: ListTile(
leading: CircleAvatar(
backgroundColor: color,
child: Icon(icon, color: Colors.white),
),
title: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: onTap,
),
);
}
}
9.2 完整淡入动画实现
class FadeInGridView extends StatefulWidget {
const FadeInGridView({super.key});
State<FadeInGridView> createState() => _FadeInGridViewState();
}
class _FadeInGridViewState extends State<FadeInGridView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
_controller.forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('淡入动画'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
itemCount: 20,
itemBuilder: (context, index) {
return FadeTransition(
opacity: _animation,
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
child: Center(
child: Text(
'Item $index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
},
),
);
}
}
9.3 完整缩放动画实现
class ScaleInGridView extends StatefulWidget {
const ScaleInGridView({super.key});
State<ScaleInGridView> createState() => _ScaleInGridViewState();
}
class _ScaleInGridViewState extends State<ScaleInGridView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
);
_controller.forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('缩放进入'),
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 18,
itemBuilder: (context, index) {
return ScaleTransition(
scale: _animation,
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
),
),
);
},
),
);
}
}
9.4 完整交错动画实现
class StaggeredGridView extends StatefulWidget {
const StaggeredGridView({super.key});
State<StaggeredGridView> createState() => _StaggeredGridViewState();
}
class _StaggeredGridViewState extends State<StaggeredGridView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final int _itemCount = 24;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
);
_controller.forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('交错动画'),
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: _itemCount,
itemBuilder: (context, index) {
// 为每个item创建独立的动画
final animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
index * 0.05, // 开始时间基于索引
(index * 0.05) + 0.5, // 每个动画持续0.5的比例
curve: Curves.easeOut,
),
),
);
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.primaries[index % Colors.primaries.length],
),
child: Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
},
),
);
}
}
9.5 完整点击反馈实现
class ClickFeedbackGrid extends StatelessWidget {
const ClickFeedbackGrid({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('点击反馈'),
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 12,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('点击了 Item $index'),
backgroundColor: Colors.purple,
duration: const Duration(seconds: 1),
),
);
},
splashColor: Colors.blue.withOpacity(0.5),
highlightColor: Colors.blue.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
child: Center(
child: Text(
'Item $index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
},
),
);
}
}
9.6 完整长按选择实现
class LongPressGrid extends StatefulWidget {
const LongPressGrid({super.key});
State<LongPressGrid> createState() => _LongPressGridState();
}
class _LongPressGridState extends State<LongPressGrid> {
final Set<int> _selectedItems = {};
bool _isSelectionMode = false;
void _toggleSelection(int index) {
setState(() {
if (_selectedItems.contains(index)) {
_selectedItems.remove(index);
} else {
_selectedItems.add(index);
}
if (_selectedItems.isEmpty) {
_isSelectionMode = false;
}
});
}
void _deleteSelected() {
setState(() {
_selectedItems.clear();
_isSelectionMode = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除选中项'),
backgroundColor: Colors.red,
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isSelectionMode
? '已选 ${_selectedItems.length} 项'
: '长按交互'),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
actions: _isSelectionMode
? [
IconButton(
icon: const Icon(Icons.delete),
onPressed: _deleteSelected,
tooltip: '删除',
),
]
: null,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 15,
itemBuilder: (context, index) {
final isSelected = _selectedItems.contains(index);
return GestureDetector(
onTap: () {
if (_isSelectionMode) {
_toggleSelection(index);
}
},
onLongPress: () {
setState(() {
_isSelectionMode = true;
_selectedItems.add(index);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已进入选择模式'),
duration: Duration(seconds: 1),
),
);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(8),
border: isSelected
? Border.all(color: Colors.white, width: 3)
: null,
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
spreadRadius: 2,
),
]
: null,
),
child: Stack(
children: [
Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
if (isSelected)
const Positioned(
top: 4,
right: 4,
child: Icon(
Icons.check_circle,
color: Colors.white,
size: 24,
),
),
],
),
),
);
},
),
);
}
}
9.7 完整Hero动画实现
class HeroGrid extends StatelessWidget {
const HeroGrid({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Hero动画'),
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 20,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(index: index),
),
);
},
child: Hero(
tag: 'hero_$index',
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
child: Center(
child: Text(
'Item $index',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
);
},
),
);
}
}
class DetailPage extends StatelessWidget {
final int index;
const DetailPage({super.key, required this.index});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('详情 $index'),
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
body: Center(
child: Hero(
tag: 'hero_$index',
child: Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.primaries[index % Colors.primaries.length],
Colors.primaries[(index + 1) % Colors.primaries.length],
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Detail $index',
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
const Text(
'这是一个详情页面',
style: TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
),
),
),
),
);
}
}
9.8 完整综合动画实现
class ComprehensiveAnimationGrid extends StatefulWidget {
const ComprehensiveAnimationGrid({super.key});
State<ComprehensiveAnimationGrid> createState() =>
_ComprehensiveAnimationGridState();
}
class _ComprehensiveAnimationGridState
extends State<ComprehensiveAnimationGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final int _itemCount = 24;
final Set<int> _selectedItems = {};
bool _isSelectionMode = false;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
)..forward();
}
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleLongPress(int index) {
setState(() {
_isSelectionMode = true;
_selectedItems.add(index);
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已进入选择模式'),
duration: Duration(seconds: 1),
),
);
}
void _handleTap(int index) {
if (_isSelectionMode) {
setState(() {
if (_selectedItems.contains(index)) {
_selectedItems.remove(index);
} else {
_selectedItems.add(index);
}
if (_selectedItems.isEmpty) {
_isSelectionMode = false;
}
});
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(index: index),
),
);
}
}
void _deleteSelected() {
setState(() {
_selectedItems.clear();
_isSelectionMode = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已删除选中项'),
backgroundColor: Colors.red,
),
);
}
Widget _buildItem(int index) {
final animation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
(index % 4) * 0.05 + (index ~/ 4) * 0.1,
0.5 + (index % 4) * 0.05 + (index ~/ 4) * 0.1,
curve: Curves.easeOutBack,
),
),
);
final isSelected = _selectedItems.contains(index);
return ScaleTransition(
scale: animation,
child: FadeTransition(
opacity: animation,
child: GestureDetector(
onTap: () => _handleTap(index),
onLongPress: () => _handleLongPress(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
border: isSelected
? Border.all(color: Colors.white, width: 4)
: null,
boxShadow: isSelected
? [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 15,
spreadRadius: 3,
),
]
: null,
),
child: Stack(
children: [
Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
if (isSelected)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(
Icons.check,
color: Colors.green,
size: 20,
),
),
),
],
),
),
),
),
);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
_isSelectionMode
? '已选 ${_selectedItems.length} 项'
: '综合动画Grid',
),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
actions: [
if (_isSelectionMode)
IconButton(
icon: const Icon(Icons.delete),
onPressed: _deleteSelected,
tooltip: '删除',
),
],
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: _itemCount,
itemBuilder: (context, index) => _buildItem(index),
),
);
}
}
10. 动画技术对比
10.1 不同动画技术对比
| 动画类型 | 实现复杂度 | 性能影响 | 适用场景 | 推荐指数 |
|---|---|---|---|---|
| 淡入动画 | ⭐ 简单 | ⭐⭐ 低 | 所有场景 | ⭐⭐⭐⭐⭐ |
| 缩放动画 | ⭐⭐ 中等 | ⭐⭐⭐ 中 | 卡片展示 | ⭐⭐⭐⭐ |
| 滑动动画 | ⭐⭐ 中等 | ⭐⭐ 低 | 列表加载 | ⭐⭐⭐⭐ |
| 交错动画 | ⭐⭐⭐ 较高 | ⭐⭐⭐ 中 | 网格展示 | ⭐⭐⭐⭐⭐ |
| Hero动画 | ⭐ 简单 | ⭐⭐⭐⭐ 较高 | 页面过渡 | ⭐⭐⭐⭐⭐ |
| 点击反馈 | ⭐ 简单 | ⭐ 极低 | 交互反馈 | ⭐⭐⭐⭐⭐ |
| 长按选择 | ⭐⭐⭐ 较高 | ⭐⭐ 低 | 多选场景 | ⭐⭐⭐⭐ |
| 视差滚动 | ⭐⭐⭐⭐ 复杂 | ⭐⭐⭐⭐⭐ 高 | 特殊效果 | ⭐⭐⭐ |
10.2 动画曲线选择指南
| 曲线名称 | 效果描述 | 适用场景 | 持续时间建议 |
|---|---|---|---|
Curves.easeInOut |
开始和结束时慢,中间快 | 大部分动画 | 300-600ms |
Curves.easeOut |
开始快,结束时慢 | 进入动画 | 300-500ms |
Curves.easeIn |
开始慢,结束时快 | 退出动画 | 300-500ms |
Curves.elasticOut |
带弹性效果 | 缩放动画 | 400-600ms |
Curves.bounceIn |
弹跳效果 | 强调动画 | 500-800ms |
Curves.decelerate |
逐渐减速 | 滚动动画 | 200-400ms |
Curves.fastOutSlowIn |
材质设计标准 | UI过渡 | 300-500ms |
11. 高级动画技巧
11.1 物理动画
物理动画基于物理规律,如弹簧、阻尼等,能创造更自然的动画效果。
class PhysicsAnimationGrid extends StatefulWidget {
const PhysicsAnimationGrid({super.key});
State<PhysicsAnimationGrid> createState() => _PhysicsAnimationGridState();
}
class _PhysicsAnimationGridState extends State<PhysicsAnimationGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
final List<double> _scales = [];
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
// 使用弹簧物理模拟
for (int i = 0; i < 12; i++) {
_scales.add(1.0);
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('物理动画'),
backgroundColor: Colors.deepOrange,
foregroundColor: Colors.white,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 12,
itemBuilder: (context, index) {
final scale = 0.8 + (_animation.value * 0.2);
return Transform.scale(
scale: scale,
child: Card(
elevation: 4,
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
},
),
);
}
}
11.2 波浪动画
波浪动画通过计算正弦波,创造连续的波动效果。
class WaveAnimationGrid extends StatefulWidget {
const WaveAnimationGrid({super.key});
State<WaveAnimationGrid> createState() => _WaveAnimationGridState();
}
class _WaveAnimationGridState extends State<WaveAnimationGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final int _itemCount = 12;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat();
}
void dispose() {
_controller.dispose();
super.dispose();
}
double _calculateWave(int index) {
// 计算正弦波
final phase = (index / _itemCount) * 2 * 3.14159;
return 0.8 + 0.2 * (1 + (3.14159 + phase).sin());
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('波浪动画'),
backgroundColor: Colors.pink,
foregroundColor: Colors.white,
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: _itemCount,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final wave = _calculateWave(index);
return Transform.scale(
scale: wave,
child: Card(
elevation: 3,
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
},
);
},
),
);
}
}
11.3 旋转动画
旋转动画可以让元素产生旋转效果,常用于加载指示器或特殊视觉效果。
class RotationAnimationGrid extends StatefulWidget {
const RotationAnimationGrid({super.key});
State<RotationAnimationGrid> createState() => _RotationAnimationGridState();
}
class _RotationAnimationGridState extends State<RotationAnimationGrid>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool _isRotating = false;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggleRotation() {
setState(() {
_isRotating = !_isRotating;
if (_isRotating) {
_controller.repeat();
} else {
_controller.stop();
_controller.reset();
}
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('旋转动画'),
backgroundColor: Colors.cyan,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: Icon(_isRotating ? Icons.stop : Icons.play_arrow),
onPressed: _toggleRotation,
),
],
),
body: GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: 12,
itemBuilder: (context, index) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _controller.value * 2 * 3.14159,
child: Card(
elevation: 3,
child: Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length],
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
);
},
);
},
),
);
}
}
12. 动画性能监控与调试
12.1 性能监控工具
Flutter提供了多种工具来监控和调试动画性能:
| 工具名称 | 用途 | 使用方法 |
|---|---|---|
| Flutter DevTools | 性能分析 | 运行 flutter pub global run devtools |
| Timeline | 帧率分析 | DevTools -> Performance -> Timeline |
| Overlay | 实时性能 | 设置 showPerformanceOverlay = true |
| Repaint Rainbow | 重绘区域 | 设置 debugRepaintRainbowEnabled = true |
12.2 性能优化检查清单
class PerformanceChecklist {
// ✅ 使用Transform而不是手动计算位置
// Transform.scale() vs Container(width: scale * originalWidth)
// ✅ 使用Opacity而不是withOpacity
// Opacity(opacity: 0.5) vs Color.fromRGBO(255, 0, 0, 0.5)
// ✅ 避免在动画中创建新对象
// ❌ 错误:每次build都创建
// BoxDecoration(color: Color.fromARGB(255, r, g, b))
// ✅ 正确:提前创建
// static const _color = Colors.blue;
// ✅ 使用const构造函数
// const Card() vs Card()
// ✅ 使用AnimatedBuilder优化重建范围
// AnimatedBuilder只重建需要的部分
// ✅ 避免在动画回调中执行复杂计算
// 将复杂计算提前完成
// ✅ 合理设置动画持续时间
// 过长会占用资源,过短会显得突兀
}
12.3 常见性能问题及解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 动画卡顿 | 太多Widget重建 | 使用RepaintBoundary隔离 |
| 内存泄漏 | AnimationController未释放 | 在dispose中释放 |
| 帧率低 | 复杂动画在主线程 | 使用GPU加速操作 |
| 启动慢 | 过多同时动画 | 使用交错动画 |
| 滚动不流畅 | 滚动时触发动画 | 使用ScrollController优化 |
13. 实战建议与最佳实践
13.1 动画设计原则
- 有目的性:每个动画都应该有明确的目的
- 自然流畅:遵循物理规律,避免突兀
- 性能优先:确保动画不影响应用性能
- 一致性:保持整个应用的动画风格统一
- 可访问性:支持动画关闭选项
13.2 常用动画时长参考
| 动画类型 | 推荐时长 | 最短 | 最长 |
|---|---|---|---|
| 微交互 | 100-200ms | 50ms | 300ms |
| 淡入淡出 | 300-400ms | 200ms | 600ms |
| 缩放动画 | 300-500ms | 200ms | 800ms |
| 滑动动画 | 300-400ms | 200ms | 600ms |
| 页面过渡 | 300-500ms | 200ms | 800ms |
| 强调动画 | 400-600ms | 300ms | 1000ms |
13.3 动画组合策略
13.4 项目中的动画管理
// 建议创建动画管理器类
class AnimationManager {
static final Map<String, AnimationController> _controllers = {};
static AnimationController create({
required String key,
required TickerProvider vsync,
required Duration duration,
}) {
if (_controllers.containsKey(key)) {
return _controllers[key]!;
}
final controller = AnimationController(
duration: duration,
vsync: vsync,
);
_controllers[key] = controller;
return controller;
}
static void dispose(String key) {
_controllers[key]?.dispose();
_controllers.remove(key);
}
static void disposeAll() {
_controllers.forEach((key, controller) => controller.dispose());
_controllers.clear();
}
}
14. 总结与展望
14.1 知识点总结
本文全面介绍了GridView动画效果的实现,从基础到高级,从理论到实践:
| 类别 | 掌握程度 | 实践建议 |
|---|---|---|
| 基础动画 | ⭐⭐⭐⭐⭐ | 熟练掌握Fade、Scale、Transform |
| 交错动画 | ⭐⭐⭐⭐ | 理解Interval原理,灵活应用 |
| 交互反馈 | ⭐⭐⭐⭐⭐ | 所有项目都应该实现 |
| Hero动画 | ⭐⭐⭐⭐ | 页面过渡必备技能 |
| 性能优化 | ⭐⭐⭐⭐ | 持续关注,不断优化 |
| 高级技巧 | ⭐⭐⭐ | 根据需求选择性学习 |
14.2 学习路径建议
14.3 扩展学习资源
| 资源类型 | 推荐资源 | 难度 |
|---|---|---|
| 官方文档 | Flutter Animation Documentation | ⭐ |
| 视频教程 | Flutter Animation YouTube | ⭐⭐ |
| 开源项目 | flutter_animations包 | ⭐⭐⭐ |
| 动画库 | flutter_staggered_animations | ⭐⭐⭐ |
| 性能工具 | Flutter DevTools | ⭐⭐⭐⭐ |
14.4 实践项目建议
- 相册应用:实现照片网格的交错加载动画
- 电商应用:商品列表的Hero动画过渡
- 音乐播放器:专辑封面的旋转动画
- 天气应用:天气图标的动态效果
- 社交应用:图片网格的点赞动画
通过这些项目的实践,您将能够熟练掌握GridView动画的各种技术,并应用到实际开发中。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)