Unverified Commit 6bdc380b authored by Mateus Felipe C. C. Pinto's avatar Mateus Felipe C. C. Pinto Committed by GitHub

Add prototypeItem property to ListView (#79752)

parent ce18d702
......@@ -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(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment