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

Flutter StickyGroupedListView滚动到指定索引回弹至顶部问题

问题背景

开发中使用sticky_grouped_list组件实现分组列表,该组件包含scrollable_positioned_list组件的全部滚动控制能力。
需求为:选中日历日期后,列表自动滚动到对应日期的索引位置。目前逻辑编写完成后出现异常:选中日期触发滚动时,列表滚动到目标索引后会立刻回弹到顶部,无法完成定位。

  • 异常效果演示:
    异常回弹效果
  • 期望效果(列表底部预留足够滚动空间,定位不回弹):
    期望正常效果
当前实现代码
class PlanificationPage extends StatefulWidget {
  const PlanificationPage({Key? key}) : super(key: key);

  @override
  State<PlanificationPage> createState() => _PlanificationPageState();
}

class _PlanificationPageState extends State<PlanificationPage> {
  final RefreshController refreshListController =
      RefreshController(initialRefresh: false);
  final GroupedItemScrollController itemScrollController =
      GroupedItemScrollController();
  final ItemPositionsListener itemPositionsListener =
      ItemPositionsListener.create();

  @override
  void initState() {
    super.initState();
  }

  moveToIndex(index) {
    if (itemScrollController.isAttached) {
      itemScrollController.scrollTo(
          index: index, duration: const Duration(milliseconds: 500));
    }
  }

  @override
  Widget build(BuildContext context) {
    final adminBloc = BlocProvider.of<AdminBloc>(context);
    return Scaffold(
        appBar: AppBar(
          centerTitle: true,
          title: Text(
            AppStrings.whiteboardProgrammingTitle,
            style: UiHelper.appBartextStyle,
          ),
        ),
        body: Column(
          children: [
            Container(
              padding: const EdgeInsets.only(top: 10),
              color: Theme.of(context).colorScheme.surface,
              child: BlocBuilder<AdminBloc, AdminState>(
                builder: (context, state) {
                  return TableCalendar(
                    firstDay: DateTime.utc(2010, 10, 16),
                    lastDay: DateTime.utc(2030, 3, 14),
                    focusedDay: state.focusedDay!,
                    calendarFormat: CalendarFormat.week,
                    startingDayOfWeek: StartingDayOfWeek.monday,
                    headerVisible: false,
                    selectedDayPredicate: (DateTime day) {
                      return isSameDay(state.selectedDay, day);
                    },
                    onDaySelected: (DateTime selectedDay, DateTime focusedDay) {
                      if (!isSameDay(state.selectedDay, selectedDay)) {
                        adminBloc.add(OnSelectedDay(
                          selectedDay: selectedDay,
                          focusedDay: focusedDay,
                        ));

                        moveToIndex(selectedDay.weekday - 1);
                      }
                    },
                    onPageChanged: (DateTime focusedDay) {
                      adminBloc
                        ..add(OnPageCalendarChangedEvent(focusedDay))
                        ..add(OnLoadWorkoutsEvent());
                    },
                    onCalendarCreated: (_) {
                      adminBloc.add(OnLoadWorkoutsEvent());
                    },
                  );
                },
              ),
            ),
            BlocBuilder<AdminBloc, AdminState>(
              builder: (context, state) {
                if (state.loadingList) {
                  return const Center(
                    child: CircularProgressIndicator(),
                  );
                }
                final List<WorkoutsByDatesModel> workouts = state.listWorkouts;
                return Expanded(
                  child: SmartRefresher(
                    controller: refreshListController,
                    enablePullDown: true,
                    onRefresh: () {
                      BlocProvider.of<AdminBloc>(context)
                          .add(OnLoadWorkoutsEvent());
                      refreshListController.refreshCompleted();
                    },
                    child: SingleChildScrollView(
                      child:
                          StickyGroupedListView<WorkoutsByDatesModel, String>(
                        shrinkWrap: true,
                        elements: workouts,
                        // 已尝试添加底部500像素padding,问题依旧存在
                        //padding: const EdgeInsets.only(bottom: 500.0),
                        // 已尝试调整physics为AlwaysScrollableScrollPhysics,无效
                        //physics: const AlwaysScrollableScrollPhysics(),
                        itemScrollController: itemScrollController,
                        groupBy: (WorkoutsByDatesModel element) =>
                            element.date.weekday.toString(),
                        groupComparator: (value1, value2) =>
                            value1.compareTo(value2),
                        groupSeparatorBuilder: (WorkoutsByDatesModel value) =>
                            Padding(
                          padding: const EdgeInsets.only(top: 10.0, bottom: 10),
                          child: Center(
                            child: Text(
                              AppUtils.getDayName(value.date.weekday),
                              style: const TextStyle(
                                fontSize: 12,
                              ),
                            ),
                          ),
                        ),
                        indexedItemBuilder: (BuildContext context,
                            WorkoutsByDatesModel element, int index) {
                         final workouts = element.workouts;
                          return ListView.builder(
                            shrinkWrap: true,
                            physics: const NeverScrollableScrollPhysics(),
                            itemCount: workouts.length + 1,
                            itemBuilder: (BuildContext context, int i) {
                              if (i == workouts.length) {
                                return GestureDetector(
                                  onTap: () {
                                    context
                                        .read<AdminBloc>()
                                        .add(OnSelectedCardDay(element.date));
                                    context.router.push(
                                        const WorkoutTypeSelectionRouter());
                                  },
                                  child: Container(
                                    color:
                                        Theme.of(context).colorScheme.surface,
                                    padding: const EdgeInsets.symmetric(
                                        horizontal: 20, vertical: 10),
                                    child: Row(
                                      children: [
                                        const Icon(
                                          Icons.add_circle_rounded,
                                          size: 16,
                                        ),
                                        const SizedBox(width: 5),
                                        Text(
                                          AppStrings.addNewWorkout,
                                          style: const TextStyle(fontSize: 16),
                                        )
                                      ],
                                    ),
                                  ),
                                );
                              } else {
                                return Slidable(
                                  endActionPane: ActionPane(
                                    motion: const ScrollMotion(),
                                    children: [
                                      SlidableAction(
                                        onPressed: (context) {
                                          // 待实现:跳转编辑训练页面
                                          print('edit');
                                        },
                                        backgroundColor: Colors.blue,
                                        foregroundColor: Colors.white,
                                        label: AppStrings.editButton,
                                      )
                                    ],
                                  ),
                                  child: Container(
                                    color:
                                        Theme.of(context).colorScheme.surface,
                                    padding: const EdgeInsets.symmetric(
                                        horizontal: 20),
                                    child: Column(
                                      crossAxisAlignment:
                                          CrossAxisAlignment.start,
                                      children: [
                                        Padding(
                                          padding: const EdgeInsets.all(4.0),
                                          child: Text(
                                              AppStrings.workoutOfDayTitle,
                                              style: TextStyle(
                                                  fontSize: 10,
                                                  color: Theme.of(context)
                                                      .disabledColor)),
                                        ),
                                        Padding(
                                          padding: const EdgeInsets.only(
                                              left: 8.0, bottom: 10),
                                          child: Text(
                                            workouts[i].title,
                                            style: const TextStyle(
                                              fontWeight: FontWeight.w500,
                                              fontSize: 16,
                                            ),
                                          ),
                                        ),
                                        Container(
                                          height: 0.5,
                                          width: double.infinity,
                                          color: Theme.of(context).dividerColor,
                                        ),
                                      ],
                                    ),
                                  ),
                                );
                              }
                            },
                          );
                        },
                      ),
                    ),
                  ),
                );
              },
            ),
          ],
        ));
  }
}
已尝试无效方案
  • 给列表添加底部padding
  • 修改列表的physics滚动属性
诉求

排查当前实现的错误点,给出可落地的解决方案,实现列表底部预留足够滚动空间、滚动到指定索引不回弹的正常效果。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 18:57:18