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'; ...@@ -21,6 +21,7 @@ import 'scroll_notification.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'sliver_prototype_extent_list.dart';
import 'viewport.dart'; import 'viewport.dart';
// Examples can assume: // Examples can assume:
...@@ -761,11 +762,19 @@ abstract class BoxScrollView extends ScrollView { ...@@ -761,11 +762,19 @@ abstract class BoxScrollView extends ScrollView {
/// children are required to fill the [ListView]. /// children are required to fill the [ListView].
/// ///
/// If non-null, the [itemExtent] forces the children to have the given extent /// 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 /// letting the children determine their own extent because the scrolling
/// machinery can make use of the foreknowledge of the children's extent to save /// machinery can make use of the foreknowledge of the children's extent to save
/// work, for example when the scroll position changes drastically. /// 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]: /// There are four options for constructing a [ListView]:
/// ///
/// 1. The default constructor takes an explicit [List<Widget>] of children. This /// 1. The default constructor takes an explicit [List<Widget>] of children. This
...@@ -951,9 +960,10 @@ abstract class BoxScrollView extends ScrollView { ...@@ -951,9 +960,10 @@ abstract class BoxScrollView extends ScrollView {
/// and [shrinkWrap] properties on [ListView] map directly to the identically /// and [shrinkWrap] properties on [ListView] map directly to the identically
/// named properties on [CustomScrollView]. /// named properties on [CustomScrollView].
/// ///
/// The [CustomScrollView.slivers] property should be a list containing either a /// The [CustomScrollView.slivers] property should be a list containing either:
/// [SliverList] or a [SliverFixedExtentList]; the former if [itemExtent] on the /// * a [SliverList] if both [itemExtent] and [prototypeItem] were null;
/// [ListView] was null, and the latter if [itemExtent] was not null. /// * a [SliverFixedExtentList] if [itemExtent] was not null; or
/// * a [SliverPrototypeExtentList] if [prototypeItem] was not null.
/// ///
/// The [childrenDelegate] property on [ListView] corresponds to the /// The [childrenDelegate] property on [ListView] corresponds to the
/// [SliverList.delegate] (or [SliverFixedExtentList.delegate]) property. The /// [SliverList.delegate] (or [SliverFixedExtentList.delegate]) property. The
...@@ -1104,6 +1114,7 @@ class ListView extends BoxScrollView { ...@@ -1104,6 +1114,7 @@ class ListView extends BoxScrollView {
bool shrinkWrap = false, bool shrinkWrap = false,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
this.itemExtent, this.itemExtent,
this.prototypeItem,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
bool addSemanticIndexes = true, bool addSemanticIndexes = true,
...@@ -1114,7 +1125,11 @@ class ListView extends BoxScrollView { ...@@ -1114,7 +1125,11 @@ class ListView extends BoxScrollView {
ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
String? restorationId, String? restorationId,
Clip clipBehavior = Clip.hardEdge, Clip clipBehavior = Clip.hardEdge,
}) : childrenDelegate = SliverChildListDelegate( }) : assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both.',
),
childrenDelegate = SliverChildListDelegate(
children, children,
addAutomaticKeepAlives: addAutomaticKeepAlives, addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries, addRepaintBoundaries: addRepaintBoundaries,
...@@ -1178,6 +1193,7 @@ class ListView extends BoxScrollView { ...@@ -1178,6 +1193,7 @@ class ListView extends BoxScrollView {
bool shrinkWrap = false, bool shrinkWrap = false,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
this.itemExtent, this.itemExtent,
this.prototypeItem,
required IndexedWidgetBuilder itemBuilder, required IndexedWidgetBuilder itemBuilder,
int? itemCount, int? itemCount,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
...@@ -1191,6 +1207,10 @@ class ListView extends BoxScrollView { ...@@ -1191,6 +1207,10 @@ class ListView extends BoxScrollView {
Clip clipBehavior = Clip.hardEdge, Clip clipBehavior = Clip.hardEdge,
}) : assert(itemCount == null || itemCount >= 0), }) : assert(itemCount == null || itemCount >= 0),
assert(semanticChildCount == null || semanticChildCount <= itemCount!), assert(semanticChildCount == null || semanticChildCount <= itemCount!),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both.',
),
childrenDelegate = SliverChildBuilderDelegate( childrenDelegate = SliverChildBuilderDelegate(
itemBuilder, itemBuilder,
childCount: itemCount, childCount: itemCount,
...@@ -1286,6 +1306,7 @@ class ListView extends BoxScrollView { ...@@ -1286,6 +1306,7 @@ class ListView extends BoxScrollView {
assert(separatorBuilder != null), assert(separatorBuilder != null),
assert(itemCount != null && itemCount >= 0), assert(itemCount != null && itemCount >= 0),
itemExtent = null, itemExtent = null,
prototypeItem = null,
childrenDelegate = SliverChildBuilderDelegate( childrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
final int itemIndex = index ~/ 2; final int itemIndex = index ~/ 2;
...@@ -1425,6 +1446,7 @@ class ListView extends BoxScrollView { ...@@ -1425,6 +1446,7 @@ class ListView extends BoxScrollView {
bool shrinkWrap = false, bool shrinkWrap = false,
EdgeInsetsGeometry? padding, EdgeInsetsGeometry? padding,
this.itemExtent, this.itemExtent,
this.prototypeItem,
required this.childrenDelegate, required this.childrenDelegate,
double? cacheExtent, double? cacheExtent,
int? semanticChildCount, int? semanticChildCount,
...@@ -1433,6 +1455,10 @@ class ListView extends BoxScrollView { ...@@ -1433,6 +1455,10 @@ class ListView extends BoxScrollView {
String? restorationId, String? restorationId,
Clip clipBehavior = Clip.hardEdge, Clip clipBehavior = Clip.hardEdge,
}) : assert(childrenDelegate != null), }) : assert(childrenDelegate != null),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
),
super( super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -1457,8 +1483,33 @@ class ListView extends BoxScrollView { ...@@ -1457,8 +1483,33 @@ class ListView extends BoxScrollView {
/// determine their own extent because the scrolling machinery can make use of /// 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 foreknowledge of the children's extent to save work, for example when
/// the scroll position changes drastically. /// 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; 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]. /// A delegate that provides the children for the [ListView].
/// ///
/// The [ListView.custom] constructor lets you specify this delegate /// The [ListView.custom] constructor lets you specify this delegate
...@@ -1474,6 +1525,11 @@ class ListView extends BoxScrollView { ...@@ -1474,6 +1525,11 @@ class ListView extends BoxScrollView {
delegate: childrenDelegate, delegate: childrenDelegate,
itemExtent: itemExtent!, itemExtent: itemExtent!,
); );
} else if (prototypeItem != null) {
return SliverPrototypeExtentList(
delegate: childrenDelegate,
prototypeItem: prototypeItem!,
);
} }
return SliverList(delegate: childrenDelegate); return SliverList(delegate: childrenDelegate);
} }
......
...@@ -261,6 +261,54 @@ void main() { ...@@ -261,6 +261,54 @@ void main() {
callbackTracker.clear(); 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 { testWidgets('ListView.separated', (WidgetTester tester) async {
Widget buildFrame({ required int itemCount }) { Widget buildFrame({ required int itemCount }) {
return Directionality( return Directionality(
......
...@@ -1231,6 +1231,13 @@ void main() { ...@@ -1231,6 +1231,13 @@ void main() {
expect(finder, findsOneWidget); 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 { testWidgets('ListView.builder asserts on negative childCount', (WidgetTester tester) async {
expect(() => ListView.builder( expect(() => ListView.builder(
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
...@@ -1260,6 +1267,28 @@ void main() { ...@@ -1260,6 +1267,28 @@ void main() {
), throwsAssertionError); ), 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 { testWidgets('PrimaryScrollController provides fallback ScrollActions', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( 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