当 ListView.builder 顶部附近的项目高度被缓存但已更改时,如何从 animateTo 获得正常行为?
Posted
技术标签:
【中文标题】当 ListView.builder 顶部附近的项目高度被缓存但已更改时,如何从 animateTo 获得正常行为?【英文标题】:When heights of items near top of the ListView.builder are cached but have changed, how to get normal behavior from animateTo? 【发布时间】:2021-08-16 23:45:02 【问题描述】:小例子
我创建了一个最小的示例,每次单击一个项目时,列表都会滚动到该项目的顶部。同时,列表可以从有标题变为没有标题。
怎么了?
如果您在向下滚动列表时保持标题打开或关闭标题,则没有任何问题。当您切换标题时会发生不良行为,而列表中的好方法。 cacheExtent 的 firstIndex 之上的某些项目将不会被重建,因此滚动偏移量将偏离那么多标题高度。
下面是示例代码:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main()
runApp(MyApp(
items: List<String>.generate(10000, (i) => 'Item $i'),
));
class MyApp extends StatefulWidget
final List<String> items;
MyApp(Key? key, required this.items) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
class _MyAppState extends State<MyApp>
final _scrollController = ScrollController();
List<bool> hasHeaders = [true, false];
int selectedIndex = 0;
Widget _headerR = Container(
height: 50,
color: Colors.amber,
child: Text('header'),
);
Widget _item(required int index)
bool isSelected = index == selectedIndex;
void _handleItemTap()
setState(()
print('setting index to $index');
selectedIndex = index;
);
double futureOffset = 100.0 * index;
if (hasHeaders[0])
futureOffset += (index + 1) * 50;
Future.delayed(Duration(milliseconds: 500), ()
_scrollController.animateTo(futureOffset,
duration: Duration(milliseconds: 500), curve: Curves.easeOut);
print('scrolling to $futureOffset');
);
return Container(
height: 100.0,
color: isSelected ? Colors.green : Colors.white,
child: Material(
color: isSelected ? Colors.green : Colors.white,
child: InkWell(
onTap: _handleItemTap,
child: ListTile(
title: Text('$widget.items[index]'),
),
),
),
);
Widget _itemBuilder(BuildContext context, int index)
if (hasHeaders[0])
return Column(
children: [_headerR, _item(index: index)],
);
else
return _item(index: index);
@override
Widget build(BuildContext context)
final title = 'Long List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
Flexible(
flex: 1,
child: ListView.builder(
controller: _scrollController,
itemCount: widget.items.length,
itemBuilder: _itemBuilder),
),
ToggleButtons(
onPressed: (int index)
setState(()
for (int buttonIndex = 0;
buttonIndex < hasHeaders.length;
buttonIndex++)
if (buttonIndex == index)
hasHeaders[buttonIndex] = true;
else
hasHeaders[buttonIndex] = false;
);
,
children: [Text('headers on'), Text('headers off')],
isSelected: hasHeaders)
],
),
),
);
【问题讨论】:
【参考方案1】:一种解决方案是在每次切换标题时重新初始化一个新的 ScrollController(具有指定的初始偏移量),并为 ListView 提供不同的 ValueKey。这将重置缓存大小计算。虽然可能不如理想的解决方案表现出色(像 React-Virtualized 的 rowHeight 回调可能会很好),但在这个简单的示例中,当它位于列表顶部时,它的性能似乎并不算太差。
当列表很长时,它的性能确实很差,尤其是当项目高度不同时。更改项目的高度可能需要几秒钟。与此问题相关:https://github.com/flutter/flutter/issues/52207
这是代码
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main()
runApp(MyApp(
items: List<String>.generate(10000, (i) => 'Item $i'),
));
class MyApp extends StatefulWidget
final List<String> items;
MyApp(Key? key, required this.items) : super(key: key);
@override
_MyAppState createState() => _MyAppState();
class _MyAppState extends State<MyApp>
late ScrollController _scrollController = ScrollController();
List<bool> hasHeaders = [true, false];
int selectedIndex = 0;
Widget _heading = Container(
height: 50,
color: Colors.amber,
child: Text('heading'),
);
Widget _item(required int index)
bool isSelected = index == selectedIndex;
void _handleItemTap()
setState(()
print('setting index to $index');
selectedIndex = index;
);
double futureOffset = 100.0 * index;
if (hasHeaders[0])
futureOffset += (index + 1) * 50;
_scrollController.animateTo(futureOffset,
duration: Duration(milliseconds: 800), curve: Curves.ease);
print('scrolling to $futureOffset');
return Container(
height: 100.0,
color: isSelected ? Colors.green : Colors.white,
child: Material(
color: isSelected ? Colors.green : Colors.white,
child: InkWell(
onTap: _handleItemTap,
child: ListTile(
title: Text('$widget.items[index]'),
),
),
),
);
Widget _itemBuilderWithHeaders(BuildContext context, int index)
return _itemBuilder(context, index, true);
Widget _itemBuilderWithoutHeaders(BuildContext context, int index)
return _itemBuilder(context, index, false);
Widget _itemBuilder(BuildContext context, int index, bool hasHeader)
if (hasHeader)
return Column(
children: [_heading, _item(index: index)],
);
else
return _item(index: index);
ListView _list(bool withHeaders)
return ListView.builder(
key: ValueKey(withHeaders ? 'listWithHeaders' : 'compactList'),
controller: _scrollController,
itemCount: widget.items.length,
itemBuilder:
withHeaders ? _itemBuilderWithHeaders : _itemBuilderWithoutHeaders);
@override
Widget build(BuildContext context)
final title = 'Long List';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
Flexible(
flex: 1,
child: hasHeaders[0] ? _list(true) : _list(false),
),
ToggleButtons(
onPressed: (int index)
double initialScrollOffset = _scrollController.offset;
if (hasHeaders[0])
int numItemsAbove = _scrollController.offset ~/ 150;
initialScrollOffset =
initialScrollOffset - (numItemsAbove + 1) * 50;
else
int numItemsAbove = _scrollController.offset ~/ 100;
initialScrollOffset =
initialScrollOffset + numItemsAbove * 50;
setState(()
for (int buttonIndex = 0;
buttonIndex < hasHeaders.length;
buttonIndex++)
if (buttonIndex == index)
hasHeaders[buttonIndex] = true;
else
hasHeaders[buttonIndex] = false;
_scrollController = ScrollController(
initialScrollOffset: initialScrollOffset);
);
,
children: [Text('headers on'), Text('headers off')],
isSelected: hasHeaders)
],
),
),
);
【讨论】:
以上是关于当 ListView.builder 顶部附近的项目高度被缓存但已更改时,如何从 animateTo 获得正常行为?的主要内容,如果未能解决你的问题,请参考以下文章
当您一直滚动到 ListView.builder 的顶部时,如何禁用动画
如何在颤动中使我的文本与 listview.builder 一起滚动
Stream 内的 Flutter ListView.Builder 不更新