Flutter实现用户点击图标后选中图标居中并隐藏其他图标
实现选中图标平滑过渡到屏幕中心的方案
核心思路
通过状态管理记录选中的图标,结合Flutter的AnimatedPositioned、AnimatedOpacity这类动画组件,实现选中图标的位置过渡动画,同时让其余图标渐隐消失。用Stack来层叠图标列表和目标位置的选中图标,保证动画过程的视觉连贯性。
具体实现步骤
1. 定义状态变量
在Add Event Page的State类中添加状态变量,用于控制动画状态和记录选中项:
// 记录选中的分类(包含图标信息) Category? selectedCategory; // 标记动画是否正在执行 bool isAnimating = false; // 屏幕中心坐标(提前计算好目标位置) late Offset centerOffset;
2. 初始化中心位置
在initState方法中计算屏幕中心的坐标,作为选中图标的最终落点:
@override void initState() { super.initState(); final screenSize = MediaQuery.of(context).size; // 假设图标大小是50x50,这里减去25让图标中心对齐屏幕中心 centerOffset = Offset(screenSize.width / 2 - 25, screenSize.height / 2 - 25); }
3. 修改Category Tile的点击逻辑
给每个Category Tile添加点击回调,触发状态更新启动动画:
// 在CategoryTile的使用处添加onTap CategoryTile( category: category, onTap: () { setState(() { selectedCategory = category; isAnimating = true; }); // 动画结束后可执行后续逻辑,比如跳转到事件编辑页 Future.delayed(const Duration(milliseconds: 500), () { // Navigator.push(context, MaterialPageRoute(builder: ...)); }); }, )
4. 用Stack和动画组件包装图标列表
把原来的图标列表用Stack包裹,根据动画状态控制每个图标的显示状态和位置:
Stack( children: [ // 原图标列表:未选中时正常显示,动画开始后渐隐消失 AnimatedOpacity( opacity: isAnimating ? 0.0 : 1.0, duration: const Duration(milliseconds: 300), child: Wrap( spacing: 16, padding: const EdgeInsets.all(16), children: categories.map((category) { return CategoryTile( category: category, onTap: () { setState(() { selectedCategory = category; isAnimating = true; }); }, ); }).toList(), ), ), // 选中的图标:动画过程中从原位置移动到屏幕中心 if (selectedCategory != null && isAnimating) AnimatedPositioned( duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, left: centerOffset.dx, top: centerOffset.dy, child: CategoryTile(category: selectedCategory!), ), ], )
5. 细节优化
- 若要精准获取图标的初始位置,可以给每个CategoryTile绑定
GlobalKey,通过key.currentContext?.findRenderObject()获取渲染位置,再转换为全局坐标计算偏移,让动画路径更准确。 - 可以更换动画曲线,比如用
Curves.elasticOut实现弹性效果,让过渡更自然。 - 动画结束后可以选择保留中心的图标,或者直接触发页面跳转。
简化版完整代码示例
class AddEventPage extends StatefulWidget { const AddEventPage({super.key}); @override State<AddEventPage> createState() => _AddEventPageState(); } class _AddEventPageState extends State<AddEventPage> { final List<Category> categories = [ Category(name: "工作", icon: Icons.work), Category(name: "生活", icon: Icons.home), Category(name: "娱乐", icon: Icons.movie), ]; Category? selectedCategory; bool isAnimating = false; late Offset centerOffset; @override void initState() { super.initState(); final screenSize = MediaQuery.of(context).size; centerOffset = Offset(screenSize.width / 2 - 25, screenSize.height / 2 - 25); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("添加事件")), body: Stack( children: [ AnimatedOpacity( opacity: isAnimating ? 0.0 : 1.0, duration: const Duration(milliseconds: 300), child: Wrap( spacing: 16, padding: const EdgeInsets.all(16), children: categories.map((category) { return CategoryTile( category: category, onTap: () { setState(() { selectedCategory = category; isAnimating = true; }); Future.delayed(const Duration(milliseconds: 500), () { // 这里写动画结束后的业务逻辑 }); }, ); }).toList(), ), ), if (selectedCategory != null && isAnimating) AnimatedPositioned( duration: const Duration(milliseconds: 500), curve: Curves.easeInOut, left: centerOffset.dx, top: centerOffset.dy, child: CategoryTile(category: selectedCategory!), ), ], ), ); } } class Category { final String name; final IconData icon; Category({required this.name, required this.icon}); } class CategoryTile extends StatelessWidget { final Category category; final VoidCallback? onTap; const CategoryTile({super.key, required this.category, this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(category.icon, size: 50), const SizedBox(height: 8), Text(category.name), ], ), ); } }
内容的提问来源于stack exchange,提问作者Jeff Barra
相关产品推荐
相关产品推荐

