You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 11:35:35