在大列表中向上滚动会导致滚动条振荡

Scrolling upwards in a big list causes the scroll to oscillate

我有一个包含 header 文本的列和一个嵌套的 StreamBuilder,通过它我构建了一个自定义项目列表,我通过我的 BloC 从 Firebase 接收到这些项目。但是我注意到,如果我向下滚动然后尝试向上滚动,无论是慢速还是快速,滚动似乎会非常快速地振荡 (up/down) 并且在向上滚动时进展非常小。

我 运行 该应用程序处于配置文件模式,并确实见证了着色器垃圾(我读到可以通过预热 Skia 着色器来修复)和其他一些延迟,但在我描述的问题期间没有任何延迟。一切都在 16 毫秒以下。 grouped_list 库似乎也没有任何活跃的相关问题,所以我不确定它是否与此有关。这是我页面的代码和视频,以更好地描述问题:

class PickUpPage extends StatefulWidget {
  const PickUpPage({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => PickUpPageState();
}

class PickUpPageState extends State<PickUpPage> {
  late PickUpScreenBloc _bloc;
  late GameDetailsBloc _detailsBloc;
  late final StreamSubscription _idsStreamSub;

  @override
  void initState() {
    super.initState();
    _bloc = PickUpScreenBloc();
    _detailsBloc = GameDetailsBloc();
    _setListeners();
  }

  void _setListeners() {
    _idsStreamSub = _bloc.idsStream.listen((ids) {
      _detailsBloc.getDetailsUsingIds(ids);
    });
  }

  @override
  void dispose() {
    _idsStreamSub.cancel();
    _bloc.dispose();
    _detailsBloc.dispose();
    super.dispose();
  }

  @override
  void deactivate() {
    _bloc.dispose();
    _detailsBloc.dispose();
    super.deactivate();
  }

  // used to rebuild the page when a user logged-in and returned to the list page
  FutureOr _onNavigateBack(dynamic val) {
    setState(() {});
  }

  void _handleGameSelected(PickUpGameDetails details) {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => GameDetailsPage(
          details: details,
        ),
      ),
    ).then((value) => _onNavigateBack(value));
  }

  @override
  Widget build(BuildContext context) {
    User? user = FirebaseAuth.instance.currentUser;
    return Column(
      children: [
        Container(
          alignment: Alignment.centerLeft,
          padding:
              const EdgeInsets.only(top: 15, left: 15, right: 15, bottom: 15),
          child: user == null
              ? const Text(
                  'Choose a pick-up game to play in:',
                  style: TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: Colors.black,
                  ),
                )
              : Text(
                  'Hey ${user.displayName == null ? '{no display name}' : user.displayName!}, choose a pick-up game to play in:',
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                    color: Colors.black,
                  ),
                ),
        ),
        const SizedBox(
          height: 10,
        ),
        BlocProvider.value(
          value: _detailsBloc,
          child: StreamBuilder<PickUpGameDetails>(
              stream: _detailsBloc.gameDetailsStream,
              builder: (context, snapshot) {
                if (!snapshot.hasError) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const CircularProgressIndicator();
                  } else {
                    return Expanded(
                      child: GroupedListView<PickUpGameDetails, String>(
                        elements: _detailsBloc.gameDetailsList,
                        sort: true,
                        order: GroupedListOrder.ASC,
                        groupComparator: (group1, group2) =>
                            group1.compareTo(group2),
                        groupBy: (gameItem) =>
                            gameItem.gameData!.dateTime!.substring(4, 8),
                        itemComparator: (item1, item2) =>
                            GameData.getGame24hTime(item1.gameData!.dateTime!)
                                .compareTo(GameData.getGame24hTime(
                                    item2.gameData!.dateTime!)),
                        indexedItemBuilder: (BuildContext context,
                                PickUpGameDetails details, int index) =>
                            InkWell(
                          splashColor: const Color(0xffff5a5f),
                          child: PickUpGameItem(
                              details.gameId!, details, Key(index.toString())),
                          onTap: () => {_handleGameSelected(details)},
                        ),
                        groupHeaderBuilder: (PickUpGameDetails details) =>
                            Padding(
                          padding: const EdgeInsets.only(
                              left: 20, top: 5, bottom: 5),
                          child: Text(
                            details.gameData!.formattedDateTime,
                            style: const TextStyle(
                                fontWeight: FontWeight.bold,
                                fontSize: 20,
                                color: Colors.black),
                          ),
                        ),
                      ),
                    );
                  }
                } else {
                  return ErrorWidget('Something went wrong!');
                }
              }),
        ),
      ],
    );
  }
}

class PickUpGameItem extends StatefulWidget {
  final String gameId;
  final PickUpGameDetails details;

  const PickUpGameItem(this.gameId, this.details, Key? key) : super(key: key);

  @override
  _PickUpGameItemState createState() => _PickUpGameItemState();
}

class _PickUpGameItemState extends State<PickUpGameItem> {
  PickUpGameDetails? _gameDetails;
  GameDetailsBloc? _detailsBloc;

  @override
  void initState() {
    super.initState();
    _detailsBloc = BlocProvider.of<GameDetailsBloc>(context);
    _detailsBloc!.subscribeToGameDetailsUpdatesWithId(widget.gameId);
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<Tuple2<String, PickUpGameDetails>>(
        stream: _detailsBloc!.detailsUpdatesStream,
        builder: (context, snapshot) {
          if (snapshot.hasError ||
              snapshot.data == null ||
              snapshot.connectionState == ConnectionState.waiting) {
            _gameDetails = widget.details;
          } else {
            if (snapshot.data!.item1 == widget.gameId) {
              _gameDetails = snapshot.data!.item2;
            } else {
              _gameDetails = widget.details;
            }
          }
          return Container(
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
            child: Row(
              children: [
                ClipRRect(
                  borderRadius: BorderRadius.circular(15.0),
                  child: _gameDetails!.locationInfo == null
                      ? const SizedBox()
                      : CachedNetworkImage(
                          imageUrl:
                              _gameDetails!.locationInfo!.pictures.elementAt(0),
                          width: 80,
                          height: 80,
                          fit: BoxFit.fill,
                          placeholder: (context, url) => const SizedBox(
                              child: Center(child: CircularProgressIndicator()),
                              width: 10,
                              height: 10),
                        ),
                ),
                const SizedBox(
                  width: 10,
                ),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        _gameDetails!.locationInfo == null
                            ? 'Loading...'
                            : _gameDetails!.locationInfo!.nam,
                        style: const TextStyle(
                            color: Colors.black,
                            fontWeight: FontWeight.bold,
                            fontSize: 16),
                        maxLines: 2,
                        overflow: TextOverflow.ellipsis,
                      ),
                      const SizedBox(
                        height: 10,
                      ),
                      _gameDetails!.gameData!.hostInfo == null
                          ? Text(
                              _gameDetails!.gameData!.gameTypeMsg!,
                              style: const TextStyle(
                                  color: Colors.black,
                                  fontWeight: FontWeight.normal,
                                  fontSize: 14),
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                            )
                          : Text(
                              '${_gameDetails!.gameData!.gameTypeMsg!} with ${_gameDetails!.gameData!.hostInfo!.hostNickname}.',
                              style: const TextStyle(
                                  color: Colors.black,
                                  fontWeight: FontWeight.normal,
                                  fontSize: 14),
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                            ),
                    ],
                  ),
                ),
                const SizedBox(
                  width: 5,
                ),
                Flexible(
                  flex: 0,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        GameData.getGameTimestamp(
                            _gameDetails!.gameData!.dateTime!),
                        style:
                            const TextStyle(color: Colors.black, fontSize: 15),
                      ),
                      const SizedBox(
                        height: 10,
                      ),
                      Row(
                        children: [
                          Text(
                            '${_gameDetails!.gameData!.getCurrentPlayerNumber()}/${_gameDetails!.gameData!.maxPlayers}',
                            style: const TextStyle(
                                color: Colors.grey,
                                fontWeight: FontWeight.normal,
                                fontSize: 14),
                          ),
                          const SizedBox(
                            width: 5,
                          ),
                          const ImageIcon(
                              AssetImage('assets/icons/profile.png'))
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          );
        });
  }
}

视频linkcan be found here

添加 mainAxisSize : MainAxisSize.min 到列小部件 添加 shrinkWrap : true 到列表视图小部件。 添加物理 NeverScrollPhysics 到列表视图。 用 singleChildScrollView 包裹该列。 如果它在底部引发溢出,则将 singleChildScrollView 包装在一个高度与设备相同、宽度与设备宽度相同的容器中。