Add prototypeItem property to ListView (#79752)
This commit is contained in:
parent
ce18d702e9
commit
6bdc380b3d
@ -21,6 +21,7 @@ import 'scroll_notification.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'sliver.dart';
|
||||
import 'sliver_prototype_extent_list.dart';
|
||||
import 'viewport.dart';
|
||||
|
||||
// Examples can assume:
|
||||
@ -761,11 +762,19 @@ abstract class BoxScrollView extends ScrollView {
|
||||
/// children are required to fill the [ListView].
|
||||
///
|
||||
/// If non-null, the [itemExtent] forces the children to have the given extent
|
||||
/// in the scroll direction. Specifying an [itemExtent] is more efficient than
|
||||
/// in the scroll direction.
|
||||
///
|
||||
/// If non-null, the [prototypeItem] forces the children to have the same extent
|
||||
/// as the given widget in the scroll direction.
|
||||
///
|
||||
/// Specifying an [itemExtent] or an [prototypeItem] is more efficient than
|
||||
/// letting the children determine their own extent because the scrolling
|
||||
/// machinery can make use of the foreknowledge of the children's extent to save
|
||||
/// work, for example when the scroll position changes drastically.
|
||||
///
|
||||
/// You can't specify both [itemExtent] and [prototypeItem], only one or none of
|
||||
/// them.
|
||||
///
|
||||
/// There are four options for constructing a [ListView]:
|
||||
///
|
||||
/// 1. The default constructor takes an explicit [List<Widget>] of children. This
|
||||
@ -951,9 +960,10 @@ abstract class BoxScrollView extends ScrollView {
|
||||
/// and [shrinkWrap] properties on [ListView] map directly to the identically
|
||||
/// named properties on [CustomScrollView].
|
||||
///
|
||||
/// The [CustomScrollView.slivers] property should be a list containing either a
|
||||
/// [SliverList] or a [SliverFixedExtentList]; the former if [itemExtent] on the
|
||||
/// [ListView] was null, and the latter if [itemExtent] was not null.
|
||||
/// The [CustomScrollView.slivers] property should be a list containing either:
|
||||
/// * a [SliverList] if both [itemExtent] and [prototypeItem] were null;
|
||||
/// * a [SliverFixedExtentList] if [itemExtent] was not null; or
|
||||
/// * a [SliverPrototypeExtentList] if [prototypeItem] was not null.
|
||||
///
|
||||
/// The [childrenDelegate] property on [ListView] corresponds to the
|
||||
/// [SliverList.delegate] (or [SliverFixedExtentList.delegate]) property. The
|
||||
@ -1104,6 +1114,7 @@ class ListView extends BoxScrollView {
|
||||
bool shrinkWrap = false,
|
||||
EdgeInsetsGeometry? padding,
|
||||
this.itemExtent,
|
||||
this.prototypeItem,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
bool addRepaintBoundaries = true,
|
||||
bool addSemanticIndexes = true,
|
||||
@ -1114,7 +1125,11 @@ class ListView extends BoxScrollView {
|
||||
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||
String? restorationId,
|
||||
Clip clipBehavior = Clip.hardEdge,
|
||||
}) : childrenDelegate = SliverChildListDelegate(
|
||||
}) : assert(
|
||||
itemExtent == null || prototypeItem == null,
|
||||
'You can only pass itemExtent or prototypeItem, not both.',
|
||||
),
|
||||
childrenDelegate = SliverChildListDelegate(
|
||||
children,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
@ -1178,6 +1193,7 @@ class ListView extends BoxScrollView {
|
||||
bool shrinkWrap = false,
|
||||
EdgeInsetsGeometry? padding,
|
||||
this.itemExtent,
|
||||
this.prototypeItem,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
int? itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
@ -1191,6 +1207,10 @@ class ListView extends BoxScrollView {
|
||||
Clip clipBehavior = Clip.hardEdge,
|
||||
}) : assert(itemCount == null || itemCount >= 0),
|
||||
assert(semanticChildCount == null || semanticChildCount <= itemCount!),
|
||||
assert(
|
||||
itemExtent == null || prototypeItem == null,
|
||||
'You can only pass itemExtent or prototypeItem, not both.',
|
||||
),
|
||||
childrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
childCount: itemCount,
|
||||
@ -1286,6 +1306,7 @@ class ListView extends BoxScrollView {
|
||||
assert(separatorBuilder != null),
|
||||
assert(itemCount != null && itemCount >= 0),
|
||||
itemExtent = null,
|
||||
prototypeItem = null,
|
||||
childrenDelegate = SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final int itemIndex = index ~/ 2;
|
||||
@ -1425,6 +1446,7 @@ class ListView extends BoxScrollView {
|
||||
bool shrinkWrap = false,
|
||||
EdgeInsetsGeometry? padding,
|
||||
this.itemExtent,
|
||||
this.prototypeItem,
|
||||
required this.childrenDelegate,
|
||||
double? cacheExtent,
|
||||
int? semanticChildCount,
|
||||
@ -1433,6 +1455,10 @@ class ListView extends BoxScrollView {
|
||||
String? restorationId,
|
||||
Clip clipBehavior = Clip.hardEdge,
|
||||
}) : assert(childrenDelegate != null),
|
||||
assert(
|
||||
itemExtent == null || prototypeItem == null,
|
||||
'You can only pass itemExtent or prototypeItem, not both',
|
||||
),
|
||||
super(
|
||||
key: key,
|
||||
scrollDirection: scrollDirection,
|
||||
@ -1457,8 +1483,33 @@ class ListView extends BoxScrollView {
|
||||
/// determine their own extent because the scrolling machinery can make use of
|
||||
/// the foreknowledge of the children's extent to save work, for example when
|
||||
/// the scroll position changes drastically.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverFixedExtentList], the sliver used internally when this property
|
||||
/// is provided. It constrains its box children to have a specific given
|
||||
/// extent along the main axis.
|
||||
/// * The [prototypeItem] property, which allows forcing the children's
|
||||
/// extent to be the same as the given widget.
|
||||
final double? itemExtent;
|
||||
|
||||
/// If non-null, forces the children to have the same extent as the given
|
||||
/// widget in the scroll direction.
|
||||
///
|
||||
/// Specifying an [prototypeItem] is more efficient than letting the children
|
||||
/// determine their own extent because the scrolling machinery can make use of
|
||||
/// the foreknowledge of the children's extent to save work, for example when
|
||||
/// the scroll position changes drastically.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverPrototypeExtentList], the sliver used internally when this
|
||||
/// property is provided. It constrains its box children to have the same
|
||||
/// extent as a prototype item along the main axis.
|
||||
/// * The [itemExtent] property, which allows forcing the children's extent
|
||||
/// to a given value.
|
||||
final Widget? prototypeItem;
|
||||
|
||||
/// A delegate that provides the children for the [ListView].
|
||||
///
|
||||
/// The [ListView.custom] constructor lets you specify this delegate
|
||||
@ -1474,6 +1525,11 @@ class ListView extends BoxScrollView {
|
||||
delegate: childrenDelegate,
|
||||
itemExtent: itemExtent!,
|
||||
);
|
||||
} else if (prototypeItem != null) {
|
||||
return SliverPrototypeExtentList(
|
||||
delegate: childrenDelegate,
|
||||
prototypeItem: prototypeItem!,
|
||||
);
|
||||
}
|
||||
return SliverList(delegate: childrenDelegate);
|
||||
}
|
||||
|
@ -261,6 +261,54 @@ void main() {
|
||||
callbackTracker.clear();
|
||||
});
|
||||
|
||||
testWidgets('ListView.builder 30 items with big jump, using prototypeItem', (WidgetTester tester) async {
|
||||
final List<int> callbackTracker = <int>[];
|
||||
|
||||
// The root view is 800x600 in the test environment and our list
|
||||
// items are 300 tall. Scrolling should cause two or three items
|
||||
// to be built.
|
||||
|
||||
Widget itemBuilder(BuildContext context, int index) {
|
||||
callbackTracker.add(index);
|
||||
return Text('$index', key: ValueKey<int>(index), textDirection: TextDirection.ltr);
|
||||
}
|
||||
|
||||
final Widget testWidget = Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: ListView.builder(
|
||||
itemBuilder: itemBuilder,
|
||||
prototypeItem: const SizedBox(
|
||||
width: 800,
|
||||
height: 300,
|
||||
),
|
||||
itemCount: 30,
|
||||
),
|
||||
);
|
||||
|
||||
void jumpTo(double newScrollOffset) {
|
||||
final ScrollableState scrollable = tester.state(find.byType(Scrollable));
|
||||
scrollable.position.jumpTo(newScrollOffset);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(testWidget);
|
||||
|
||||
// 2 is in the cache area, but not visible.
|
||||
expect(callbackTracker, equals(<int>[0, 1, 2]));
|
||||
final List<int> initialExpectedHidden = List<int>.generate(28, (int i) => i + 2);
|
||||
check(visible: <int>[0, 1], hidden: initialExpectedHidden);
|
||||
callbackTracker.clear();
|
||||
|
||||
// Jump to the end of the ListView.
|
||||
jumpTo(8400);
|
||||
await tester.pump();
|
||||
|
||||
// 27 is in the cache area, but not visible.
|
||||
expect(callbackTracker, equals(<int>[27, 28, 29]));
|
||||
final List<int> finalExpectedHidden = List<int>.generate(28, (int i) => i);
|
||||
check(visible: <int>[28, 29], hidden: finalExpectedHidden);
|
||||
callbackTracker.clear();
|
||||
});
|
||||
|
||||
testWidgets('ListView.separated', (WidgetTester tester) async {
|
||||
Widget buildFrame({ required int itemCount }) {
|
||||
return Directionality(
|
||||
|
@ -1231,6 +1231,13 @@ void main() {
|
||||
expect(finder, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('ListView asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async {
|
||||
expect(() => ListView(
|
||||
itemExtent: 100,
|
||||
prototypeItem: const SizedBox(),
|
||||
), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('ListView.builder asserts on negative childCount', (WidgetTester tester) async {
|
||||
expect(() => ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
@ -1260,6 +1267,28 @@ void main() {
|
||||
), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('ListView.builder asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async {
|
||||
expect(() => ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return const SizedBox();
|
||||
},
|
||||
itemExtent: 100,
|
||||
prototypeItem: const SizedBox(),
|
||||
), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('ListView.custom asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async {
|
||||
expect(() => ListView.custom(
|
||||
childrenDelegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
itemExtent: 100,
|
||||
prototypeItem: const SizedBox(),
|
||||
), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('PrimaryScrollController provides fallback ScrollActions', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
Loading…
x
Reference in New Issue
Block a user