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

Flutter PopupMenuButton动态列表添加高度约束后动画不同步问题

Flutter PopupMenuButton动态列表动画不同步问题

问题场景

我用PopupMenuButton实现动态任务列表,原本每个菜单项的文本显隐会和菜单开合动画同步。但因为列表过长,添加constraints: const BoxConstraints(maxHeight: 500)限制高度后,打开/关闭菜单时,菜单项文本直接显示/消失,动画和菜单完全不同步。但换成静态硬编码的列表时,动画表现正常。

核心代码

动态列表实现(存在动画问题)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wideview/view_models/task_control_view_model.dart';
import 'package:wideview/widgets/gap.dart';

class TaskPopupMenuView extends ConsumerWidget {
  const TaskPopupMenuView({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final taskControlViewModel = ref.watch(taskControlViewModelProvider);
    List<Task>? tasks = taskControlViewModel.tasks;

    return PopupMenuButton<Task>(
      offset: const Offset(120, 50),
      // 添加该约束后出现动画不同步问题
      constraints: const BoxConstraints(
        maxHeight: 500,
      ),
      icon: const TaskIcon(),
      onSelected: (item) {
        if (item.isSelected) {
          ref.read(taskControlViewModelProvider.notifier).setNoSelectedTask();
        } else {
          ref.read(taskControlViewModelProvider.notifier).setSelectedTask(item);
        }
      },
      itemBuilder: (context) {
        // 基于tasks动态生成菜单项
        return tasks?.map((Task task) {
              return PopupMenuItem<Task>(
                value: task,
                child: TaskPopupView(task: task),
              );
            }).toList() ??
            [];
      },
    );
  }
}

静态列表实现(动画正常)

// 替换上述itemBuilder为以下静态列表,动画表现正常
itemBuilder: (context) => [
  PopupMenuItem<Task>(
      value: taskControlViewModel.selectedTask,
      child: TaskPopupView(task: taskControlViewModel.selectedTask!)),
  PopupMenuItem<Task>(
      value: taskControlViewModel.selectedTask,
      child: TaskPopupView(task: taskControlViewModel.selectedTask!)),
  PopupMenuItem<Task>(
      value: taskControlViewModel.selectedTask,
      child: TaskPopupView(task: taskControlViewModel.selectedTask!)),
],

已尝试的无效方案

  • 自定义PopupMenuButton,问题未解决;
  • 为每个列表项添加独立动画,仅打开菜单时有效,关闭时失效;
  • 用ConstrainedBox包裹PopupMenuButton,无任何效果。

问题原因

  1. Widget复用失效:动态生成的PopupMenuItem每次build都会通过map().toList()创建新的列表实例,Flutter无法复用之前的Widget状态,导致动画衔接断裂。
  2. 约束导致的Widget树变化:添加constraints后,PopupMenu内部会用ConstrainedBox包裹列表内容,动态项在菜单关闭时会被直接从Widget树移除,无法参与菜单的缩放/淡入淡出动画。而静态列表的项是固定存在的,Widget树结构稳定,动画能正常关联。

修复方案

方案1:给动态项添加唯一Key

为每个PopupMenuItem绑定基于Task唯一标识的Key,让Flutter能够识别并复用Widget,维持动画状态:

itemBuilder: (context) {
  return tasks?.map((Task task) {
        return PopupMenuItem<Task>(
          key: ValueKey(task.id), // 使用Task的唯一ID作为Key
          value: task,
          child: TaskPopupView(task: task),
        );
      }).toList() ??
      [];
},

方案2:自定义showMenu替代PopupMenuButton

直接使用showMenu方法手动创建菜单,更灵活控制列表和动画:

@override
Widget build(BuildContext context, WidgetRef ref) {
  final taskControlViewModel = ref.watch(taskControlViewModelProvider);
  List<Task>? tasks = taskControlViewModel.tasks;

  return IconButton(
    icon: const TaskIcon(),
    onPressed: () async {
      final selectedTask = await showMenu<Task>(
        context: context,
        position: RelativeRect.fromLTRB(120, 50, 0, 0), // 对应原offset
        items: tasks?.map((task) => PopupMenuItem<Task>(
              key: ValueKey(task.id),
              value: task,
              child: TaskPopupView(task: task),
            )).toList() ?? [],
        constraints: const BoxConstraints(maxHeight: 500),
      );

      if (selectedTask != null) {
        // 处理选中逻辑
        if (selectedTask.isSelected) {
          ref.read(taskControlViewModelProvider.notifier).setNoSelectedTask();
        } else {
          ref.read(taskControlViewModelProvider.notifier).setSelectedTask(selectedTask);
        }
      }
    },
  );
}

方案3:统一动画控制器

确保TaskPopupView内部的文本动画绑定到PopupMenu的动画控制器,而非独立动画。可以通过PopupMenuTheme.of(context)获取主题中的动画参数,或在TaskPopupView中接收父组件传递的动画值:

// 修改TaskPopupView,接收动画参数
class TaskPopupView extends StatelessWidget {
  final Task task;
  final Animation<double>? animation; // 新增动画参数

  const TaskPopupView({super.key, required this.task, this.animation});

  @override
  Widget build(BuildContext context) {
    // 使用动画控制文本显隐
    return FadeTransition(
      opacity: animation ?? AlwaysStoppedAnimation(1),
      child: Text(task.title),
    );
  }
}

// 在PopupMenuItem中传递动画
return PopupMenuItem<Task>(
  key: ValueKey(task.id),
  value: task,
  child: TaskPopupView(
    task: task,
    animation: AnimationProvider.of(context), // 从上下文获取PopupMenu的动画
  ),
);

内容的提问来源于stack exchange,提问作者Nir Pikan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 07:47:34