在大列表中向上滚动会导致滚动条振荡
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 包装在一个高度与设备相同、宽度与设备宽度相同的容器中。
我有一个包含 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 包装在一个高度与设备相同、宽度与设备宽度相同的容器中。