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

使用SizeTransition实现Flutter列表增删动画后出现移除项闪烁问题

列表项增删动画Widget的闪烁问题求助

我实现了一个用于列表项增删时展示动画效果的Widget,尝试过AnimatedList但发现它和flutter_hooks不兼容,所以自己写了以下实现:

class AnimatedTasksListByRating extends HookWidget {
  const AnimatedTasksListByRating(
      {super.key,
      required this.taskList,
      this.splashColor,
      this.checkboxActiveColor});

  final List<Task> taskList;
  final Color? splashColor;
  final Color? checkboxActiveColor;

  final Duration animationDuration = const Duration(milliseconds: 400);

  @override
  Widget build(BuildContext context) {
    //TODO: fix edge-case: quickly tapping the filter chips messes with the animation
    //TODO: optimize - even checking one task rebuilds all of the lists

    //separate to 2 lists - so we can calculate the diff and animate accordingly
    final displayListState = useState<List<Task>>(taskList);

    //used to mark which items need to animate
    final indexToRemove = <int>[];
    final indexToAdd = <int>[];

    //animation controllers for adding and removing items
    final animController =
        useAnimationController(duration: animationDuration, initialValue: 1);

    final addAnim = Tween(begin: 0.0, end: 1.0).animate(
        CurvedAnimation(parent: animController, curve: Curves.easeInOutCubic));

    final removeAnim = Tween(begin: 1.0, end: 0.0).animate(
        CurvedAnimation(parent: animController, curve: Curves.easeInOutCubic));

    final columnChildren = <Widget>[];

    //check if animating from empty state
    if (taskList.isNotEmpty && displayListState.value.isEmpty) {
      displayListState.value.add(Task.placeholder);
    }

    //find all tasks that have been added, and call insertItem on them.
    for (int i = 0; i < taskList.length; i++) {
      if (!displayListState.value.contains(taskList[i])) {
        displayListState.value.insert(i, taskList[i]);
        indexToAdd.add(i);
      }
    }

    //find all tasks that have been removed, and call removeItem on them.
    for (int i = 0; i < displayListState.value.length; i++) {
      if (!taskList.contains(displayListState.value[i])) {
        indexToRemove.add(i);
      }
    }

    //animating to empty state
    if (taskList.isEmpty && displayListState.value.isNotEmpty) {
      displayListState.value.add(Task.placeholder);
    }

    if (displayListState.value.isEmpty) {
      return ListTile(
        title: Text(
          'No Tasks Here',
          style: Theme.of(context).textTheme.titleMedium,
        ),
      );
    }

    for (int i = 0; i < displayListState.value.length; i++) {
      Widget toAdd;
      if (displayListState.value[i] == Task.placeholder) {
        //if we are moving to empty state - use the adding animation.
        //if we are moving from the empty state - use the removing animation
        final animToUse = indexToRemove.contains(i) ? removeAnim : addAnim;
        toAdd = SizeTransition(
          sizeFactor: animToUse,
          child: ListTile(
            title: Text(
              'No Tasks Here',
              style: Theme.of(context).textTheme.titleMedium,
            ),
          ),
        );
      } else if (indexToRemove.contains(i)) {
        toAdd = SizeTransition(
          sizeFactor: removeAnim,
          child: TaskListItem(
            splashColor: splashColor,
            checkboxActiveColor: checkboxActiveColor,
            task: displayListState.value[i],
          ),
        );
      } else if (indexToAdd.contains(i)) {
        toAdd = SizeTransition(
          sizeFactor: addAnim,
          child: TaskListItem(
            splashColor: splashColor,
            checkboxActiveColor: checkboxActiveColor,
            task: displayListState.value[i],
          ),
        );
      } else {
        toAdd = TaskListItem(
          splashColor: splashColor,
          checkboxActiveColor: checkboxActiveColor,
          task: displayListState.value[i],
        );
      }

      columnChildren.add(toAdd);
    }

    if (indexToAdd.isNotEmpty || indexToRemove.isNotEmpty) {
      final currentState = 1 - animController.value;

      animController.forward(from: currentState).whenCompleteOrCancel(() {
        displayListState.value = taskList;
      });
    }

    return Column(children: columnChildren);
  }
}

这个Widget的逻辑是对比数据列表taskList和当前显示列表displayListState.value,标记需要增删的项并执行动画。整体运行正常,但存在一个问题:有时已移除的项会闪烁回显几帧后才消失。

我在动画完成后的whenCompleteOrCancel回调里把displayListState.value设为taskList,猜测可能是回调延迟导致的这个问题,但不确定具体原因,想请教解决办法。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 04:07:03