Unverified Commit a9fac733 authored by xubaolin's avatar xubaolin Committed by GitHub

[New feature] Allowing the `ListView` slivers to have different extents while...

[New feature] Allowing the `ListView` slivers to have different extents while still having scrolling performance (#131393)

Fixes https://github.com/flutter/flutter/issues/113431

Currently we only support specifying all slivers to have the same extent.
This patch introduces an `itemExtentBuilder` property for `ListView`, allowing the slivers to have different extents while still having scrolling performance, especially when the scroll position changes drastically(such as scrolling by the scrollbar or controller.jumpTo()).

@Piinks Hi, Any thoughts about this?  :)
parent e645fb7d
......@@ -5,6 +5,7 @@
import 'dart:ui' show lerpDouble;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'debug.dart';
......@@ -76,6 +77,7 @@ class ReorderableListView extends StatefulWidget {
this.onReorderStart,
this.onReorderEnd,
this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem,
this.proxyDecorator,
this.buildDefaultDragHandles = true,
......@@ -96,8 +98,10 @@ class ReorderableListView extends StatefulWidget {
this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar,
}) : assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
),
assert(
children.every((Widget w) => w.key != null),
......@@ -142,6 +146,7 @@ class ReorderableListView extends StatefulWidget {
this.onReorderStart,
this.onReorderEnd,
this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem,
this.proxyDecorator,
this.buildDefaultDragHandles = true,
......@@ -163,8 +168,10 @@ class ReorderableListView extends StatefulWidget {
this.autoScrollerVelocityScalar,
}) : assert(itemCount >= 0),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
);
/// {@macro flutter.widgets.reorderable_list.itemBuilder}
......@@ -269,6 +276,9 @@ class ReorderableListView extends StatefulWidget {
/// {@macro flutter.widgets.list_view.itemExtent}
final double? itemExtent;
/// {@macro flutter.widgets.list_view.itemExtentBuilder}
final ItemExtentBuilder? itemExtentBuilder;
/// {@macro flutter.widgets.list_view.prototypeItem}
final Widget? prototypeItem;
......@@ -440,6 +450,7 @@ class _ReorderableListViewState extends State<ReorderableListView> {
sliver: SliverReorderableList(
itemBuilder: _itemBuilder,
itemExtent: widget.itemExtent,
itemExtentBuilder: widget.itemExtentBuilder,
prototypeItem: widget.prototypeItem,
itemCount: widget.itemCount,
onReorder: widget.onReorder,
......
......@@ -16,6 +16,71 @@ import 'viewport_offset.dart';
// CORE TYPES FOR SLIVERS
// The RenderSliver base class and its helper types.
/// Called to get the item extent by the index of item.
///
/// Used by [ListView.itemExtentBuilder] and [SliverVariedExtentList.itemExtentBuilder].
typedef ItemExtentBuilder = double Function(int index, SliverLayoutDimensions dimensions);
/// Relates the dimensions of the [RenderSliver] during layout.
///
/// Used by [ListView.itemExtentBuilder] and [SliverVariedExtentList.itemExtentBuilder].
@immutable
class SliverLayoutDimensions {
/// Constructs a [SliverLayoutDimensions] with the specified parameters.
const SliverLayoutDimensions({
required this.scrollOffset,
required this.precedingScrollExtent,
required this.viewportMainAxisExtent,
required this.crossAxisExtent
});
/// {@macro flutter.rendering.SliverConstraints.scrollOffset}
final double scrollOffset;
/// {@macro flutter.rendering.SliverConstraints.precedingScrollExtent}
final double precedingScrollExtent;
/// The number of pixels the viewport can display in the main axis.
///
/// For a vertical list, this is the height of the viewport.
final double viewportMainAxisExtent;
/// The number of pixels in the cross-axis.
///
/// For a vertical list, this is the width of the sliver.
final double crossAxisExtent;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other is! SliverLayoutDimensions) {
return false;
}
return other.scrollOffset == scrollOffset &&
other.precedingScrollExtent == precedingScrollExtent &&
other.viewportMainAxisExtent == viewportMainAxisExtent &&
other.crossAxisExtent == crossAxisExtent;
}
@override
String toString() {
return 'scrollOffset: $scrollOffset'
' precedingScrollExtent: $precedingScrollExtent'
' viewportMainAxisExtent: $viewportMainAxisExtent'
' crossAxisExtent: $crossAxisExtent';
}
@override
int get hashCode => Object.hash(
scrollOffset,
precedingScrollExtent,
viewportMainAxisExtent,
viewportMainAxisExtent
);
}
/// The direction in which a sliver's contents are ordered, relative to the
/// scroll offset axis.
///
......@@ -224,12 +289,13 @@ class SliverConstraints extends Constraints {
/// {@macro flutter.rendering.ScrollDirection.sample}
final ScrollDirection userScrollDirection;
/// {@template flutter.rendering.SliverConstraints.scrollOffset}
/// The scroll offset, in this sliver's coordinate system, that corresponds to
/// the earliest visible part of this sliver in the [AxisDirection] if
/// [growthDirection] is [GrowthDirection.forward] or in the opposite
/// [AxisDirection] direction if [growthDirection] is [GrowthDirection.reverse].
/// [SliverConstraints.growthDirection] is [GrowthDirection.forward] or in the opposite
/// [AxisDirection] direction if [SliverConstraints.growthDirection] is [GrowthDirection.reverse].
///
/// For example, if [AxisDirection] is [AxisDirection.down] and [growthDirection]
/// For example, if [AxisDirection] is [AxisDirection.down] and [SliverConstraints.growthDirection]
/// is [GrowthDirection.forward], then scroll offset is the amount the top of
/// the sliver has been scrolled past the top of the viewport.
///
......@@ -240,7 +306,7 @@ class SliverConstraints extends Constraints {
///
/// For slivers whose top is not past the top of the viewport, the
/// [scrollOffset] is `0` when [AxisDirection] is [AxisDirection.down] and
/// [growthDirection] is [GrowthDirection.forward]. The set of slivers with
/// [SliverConstraints.growthDirection] is [GrowthDirection.forward]. The set of slivers with
/// [scrollOffset] `0` includes all the slivers that are below the bottom of the
/// viewport.
///
......@@ -249,9 +315,11 @@ class SliverConstraints extends Constraints {
/// partially 'protrude in' from the bottom of the viewport.
///
/// Whether this corresponds to the beginning or the end of the sliver's
/// contents depends on the [growthDirection].
/// contents depends on the [SliverConstraints.growthDirection].
/// {@endtemplate}
final double scrollOffset;
/// {@template flutter.rendering.SliverConstraints.precedingScrollExtent}
/// The scroll distance that has been consumed by all [RenderSliver]s that
/// came before this [RenderSliver].
///
......@@ -273,6 +341,7 @@ class SliverConstraints extends Constraints {
/// content forever without reaching the end. For any [RenderSliver]s that
/// appear after the infinite [RenderSliver], the [precedingScrollExtent] will
/// be [double.infinity].
/// {@endtemplate}
final double precedingScrollExtent;
/// The number of pixels from where the pixels corresponding to the
......
......@@ -21,6 +21,7 @@ import 'scrollable.dart';
import 'scrollable_helpers.dart';
import 'sliver.dart';
import 'sliver_prototype_extent_list.dart';
import 'sliver_varied_extent_list.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
......@@ -118,6 +119,7 @@ class ReorderableList extends StatefulWidget {
this.onReorderStart,
this.onReorderEnd,
this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem,
this.proxyDecorator,
this.padding,
......@@ -135,10 +137,12 @@ class ReorderableList extends StatefulWidget {
this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar,
}) : assert(itemCount >= 0),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
);
assert(
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
);
/// {@template flutter.widgets.reorderable_list.itemBuilder}
/// Called, as needed, to build list item widgets.
......@@ -253,6 +257,9 @@ class ReorderableList extends StatefulWidget {
/// {@macro flutter.widgets.list_view.itemExtent}
final double? itemExtent;
/// {@macro flutter.widgets.list_view.itemExtentBuilder}
final ItemExtentBuilder? itemExtentBuilder;
/// {@macro flutter.widgets.list_view.prototypeItem}
final Widget? prototypeItem;
......@@ -450,14 +457,17 @@ class SliverReorderableList extends StatefulWidget {
this.onReorderStart,
this.onReorderEnd,
this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem,
this.proxyDecorator,
double? autoScrollerVelocityScalar,
}) : autoScrollerVelocityScalar = autoScrollerVelocityScalar ?? _kDefaultAutoScrollVelocityScalar,
assert(itemCount >= 0),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
);
// An eyeballed value for a smooth scrolling experience.
......@@ -487,6 +497,9 @@ class SliverReorderableList extends StatefulWidget {
/// {@macro flutter.widgets.list_view.itemExtent}
final double? itemExtent;
/// {@macro flutter.widgets.list_view.itemExtentBuilder}
final ItemExtentBuilder? itemExtentBuilder;
/// {@macro flutter.widgets.list_view.prototypeItem}
final Widget? prototypeItem;
......@@ -1036,6 +1049,11 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
delegate: childrenDelegate,
itemExtent: widget.itemExtent!,
);
} else if (widget.itemExtentBuilder != null) {
return SliverVariedExtentList(
delegate: childrenDelegate,
itemExtentBuilder: widget.itemExtentBuilder!,
);
} else if (widget.prototypeItem != null) {
return SliverPrototypeExtentList(
delegate: childrenDelegate,
......
......@@ -24,6 +24,7 @@ import 'scrollable.dart';
import 'scrollable_helpers.dart';
import 'sliver.dart';
import 'sliver_prototype_extent_list.dart';
import 'sliver_varied_extent_list.dart';
import 'viewport.dart';
// Examples can assume:
......@@ -1230,6 +1231,7 @@ class ListView extends BoxScrollView {
super.shrinkWrap,
super.padding,
this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
......@@ -1242,8 +1244,10 @@ class ListView extends BoxScrollView {
super.restorationId,
super.clipBehavior,
}) : assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both.',
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
),
childrenDelegate = SliverChildListDelegate(
children,
......@@ -1303,6 +1307,7 @@ class ListView extends BoxScrollView {
super.shrinkWrap,
super.padding,
this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem,
required NullableIndexedWidgetBuilder itemBuilder,
ChildIndexGetter? findChildIndexCallback,
......@@ -1319,8 +1324,10 @@ class ListView extends BoxScrollView {
}) : assert(itemCount == null || itemCount >= 0),
assert(semanticChildCount == null || semanticChildCount <= itemCount!),
assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both.',
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
),
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
......@@ -1408,6 +1415,7 @@ class ListView extends BoxScrollView {
super.clipBehavior,
}) : assert(itemCount >= 0),
itemExtent = null,
itemExtentBuilder = null,
prototypeItem = null,
childrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) {
......@@ -1528,6 +1536,7 @@ class ListView extends BoxScrollView {
super.padding,
this.itemExtent,
this.prototypeItem,
this.itemExtentBuilder,
required this.childrenDelegate,
super.cacheExtent,
super.semanticChildCount,
......@@ -1536,8 +1545,10 @@ class ListView extends BoxScrollView {
super.restorationId,
super.clipBehavior,
}) : assert(
itemExtent == null || prototypeItem == null,
'You can only pass itemExtent or prototypeItem, not both',
(itemExtent == null && prototypeItem == null) ||
(itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
);
/// {@template flutter.widgets.list_view.itemExtent}
......@@ -1556,9 +1567,38 @@ class ListView extends BoxScrollView {
/// extent along the main axis.
/// * The [prototypeItem] property, which allows forcing the children's
/// extent to be the same as the given widget.
/// * The [itemExtentBuilder] property, which allows forcing the children's
/// extent to be the value returned by the callback.
/// {@endtemplate}
final double? itemExtent;
/// {@template flutter.widgets.list_view.itemExtentBuilder}
/// If non-null, forces the children to have the corresponding extent returned
/// by the builder.
///
/// Specifying an [itemExtentBuilder] 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.
///
/// This will be called multiple times during the layout phase of a frame to find
/// the items that should be loaded by the lazy loading process.
///
/// Unlike [itemExtent] or [prototypeItem], this allows children to have
/// different extents.
///
/// See also:
///
/// * [SliverVariedExtentList], 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 [itemExtent] property, which allows forcing the children's extent
/// to a given value.
/// * The [prototypeItem] property, which allows forcing the children's
/// extent to be the same as the given widget.
/// {@endtemplate}
final ItemExtentBuilder? itemExtentBuilder;
/// {@template flutter.widgets.list_view.prototypeItem}
/// If non-null, forces the children to have the same extent as the given
/// widget in the scroll direction.
......@@ -1575,6 +1615,8 @@ class ListView extends BoxScrollView {
/// extent as a prototype item along the main axis.
/// * The [itemExtent] property, which allows forcing the children's extent
/// to a given value.
/// * The [itemExtentBuilder] property, which allows forcing the children's
/// extent to be the value returned by the callback.
/// {@endtemplate}
final Widget? prototypeItem;
......@@ -1593,6 +1635,11 @@ class ListView extends BoxScrollView {
delegate: childrenDelegate,
itemExtent: itemExtent!,
);
} else if (itemExtentBuilder != null) {
return SliverVariedExtentList(
delegate: childrenDelegate,
itemExtentBuilder: itemExtentBuilder!,
);
} else if (prototypeItem != null) {
return SliverPrototypeExtentList(
delegate: childrenDelegate,
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'scroll_delegate.dart';
import 'sliver.dart';
/// A sliver that places its box children in a linear array and constrains them
/// to have the corresponding extent returned by [itemExtentBuilder].
///
/// _To learn more about slivers, see [CustomScrollView.slivers]._
///
/// [SliverVariedExtentList] arranges its children in a line along
/// the main axis starting at offset zero and without gaps. Each child is
/// constrained to the corresponding extent along the main axis
/// and the [SliverConstraints.crossAxisExtent] along the cross axis.
///
/// [SliverVariedExtentList] is more efficient than [SliverList] because
/// [SliverVariedExtentList] does not need to lay out its children to obtain
/// their extent along the main axis. It's a little more flexible than
/// [SliverFixedExtentList] because this allow the children to have different extents.
///
/// See also:
///
/// * [SliverFixedExtentList], whose children are forced to a given pixel
/// extent.
/// * [SliverList], which does not require its children to have the same
/// extent in the main axis.
/// * [SliverFillViewport], which sizes its children based on the
/// size of the viewport, regardless of what else is in the scroll view.
class SliverVariedExtentList extends SliverMultiBoxAdaptorWidget {
/// Creates a sliver that places box children with the same main axis extent
/// in a linear array.
const SliverVariedExtentList({
super.key,
required super.delegate,
required this.itemExtentBuilder,
});
/// A sliver that places multiple box children in a linear array along the main
/// axis.
///
/// [SliverVariedExtentList] places its children in a linear array along the main
/// axis starting at offset zero and without gaps. Each child is forced to have
/// the returned extent of [itemExtentBuilder] in the main axis and the
/// [SliverConstraints.crossAxisExtent] in the cross axis.
///
/// This constructor is appropriate for sliver lists with a large (or
/// infinite) number of children whose extent is already determined.
///
/// Providing a non-null `itemCount` improves the ability of the [SliverGrid]
/// to estimate the maximum scroll extent.
SliverVariedExtentList.builder({
super.key,
required NullableIndexedWidgetBuilder itemBuilder,
required this.itemExtentBuilder,
ChildIndexGetter? findChildIndexCallback,
int? itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
}) : super(delegate: SliverChildBuilderDelegate(
itemBuilder,
findChildIndexCallback: findChildIndexCallback,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
));
/// A sliver that places multiple box children in a linear array along the main
/// axis.
///
/// [SliverVariedExtentList] places its children in a linear array along the main
/// axis starting at offset zero and without gaps. Each child is forced to have
/// the returned extent of [itemExtentBuilder] in the main axis and the
/// [SliverConstraints.crossAxisExtent] in the cross axis.
///
/// This constructor uses a list of [Widget]s to build the sliver.
SliverVariedExtentList.list({
super.key,
required List<Widget> children,
required this.itemExtentBuilder,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
}) : super(delegate: SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
));
/// The children extent builder.
final ItemExtentBuilder itemExtentBuilder;
@override
RenderSliverVariedExtentList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
return RenderSliverVariedExtentList(childManager: element, itemExtentBuilder: itemExtentBuilder);
}
@override
void updateRenderObject(BuildContext context, RenderSliverVariedExtentList renderObject) {
renderObject.itemExtentBuilder = itemExtentBuilder;
}
}
/// A sliver that places multiple box children with the corresponding main axis extent in
/// a linear array.
class RenderSliverVariedExtentList extends RenderSliverFixedExtentBoxAdaptor {
/// Creates a sliver that contains multiple box children that have a explicit
/// extent in the main axis.
///
/// The [childManager] argument must not be null.
RenderSliverVariedExtentList({
required super.childManager,
required ItemExtentBuilder itemExtentBuilder,
}) : _itemExtentBuilder = itemExtentBuilder;
@override
ItemExtentBuilder get itemExtentBuilder => _itemExtentBuilder;
ItemExtentBuilder _itemExtentBuilder;
set itemExtentBuilder(ItemExtentBuilder value) {
if (_itemExtentBuilder == value) {
return;
}
_itemExtentBuilder = value;
markNeedsLayout();
}
@override
double? get itemExtent => null;
}
......@@ -136,6 +136,7 @@ export 'src/widgets/sliver_fill.dart';
export 'src/widgets/sliver_layout_builder.dart';
export 'src/widgets/sliver_persistent_header.dart';
export 'src/widgets/sliver_prototype_extent_list.dart';
export 'src/widgets/sliver_varied_extent_list.dart';
export 'src/widgets/slotted_render_object_widget.dart';
export 'src/widgets/snapshot_widget.dart';
export 'src/widgets/spacer.dart';
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -776,4 +777,189 @@ void main() {
final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first;
expect(renderObject.clipBehavior, equals(Clip.antiAlias));
});
// Regression test for https://github.com/flutter/flutter/pull/131393
testWidgets('itemExtentBuilder test', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
final List<int> buildLog = <int>[];
late SliverLayoutDimensions sliverLayoutDimensions;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListView.builder(
controller: controller,
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
sliverLayoutDimensions = dimensions;
return 100.0;
},
itemBuilder: (BuildContext context, int index) {
buildLog.insert(0, index);
return Text('Item $index');
},
),
),
);
expect(find.text('Item 0'), findsOneWidget);
expect(find.text('Item 5'), findsOneWidget);
expect(find.text('Item 6'), findsNothing);
expect(
sliverLayoutDimensions,
const SliverLayoutDimensions(
scrollOffset: 0.0,
precedingScrollExtent: 0.0,
viewportMainAxisExtent: 600.0,
crossAxisExtent: 800.0,
)
);
// viewport(600.0) + cache extent after(250.0)
expect(buildLog.length, 9);
expect(buildLog.min, 0);
expect(buildLog.max, 8);
buildLog.clear();
// Scrolling drastically.
controller.jumpTo(10000.0);
await tester.pump();
expect(find.text('Item 99'), findsNothing);
expect(find.text('Item 100'), findsOneWidget);
expect(find.text('Item 105'), findsOneWidget);
expect(find.text('Item 106'), findsNothing);
expect(
sliverLayoutDimensions,
const SliverLayoutDimensions(
scrollOffset: 10000.0,
precedingScrollExtent: 0.0,
viewportMainAxisExtent: 600.0,
crossAxisExtent: 800.0,
)
);
// Scrolling drastically only loading the visible and cached area items.
// cache extent before(250.0) + viewport(600.0) + cache extent after(250.0)
expect(buildLog.length, 12);
expect(buildLog.min, 97);
expect(buildLog.max, 108);
buildLog.clear();
controller.jumpTo(5000.0);
await tester.pump();
expect(find.text('Item 49'), findsNothing);
expect(find.text('Item 50'), findsOneWidget);
expect(find.text('Item 55'), findsOneWidget);
expect(find.text('Item 56'), findsNothing);
expect(
sliverLayoutDimensions,
const SliverLayoutDimensions(
scrollOffset: 5000.0,
precedingScrollExtent: 0.0,
viewportMainAxisExtent: 600.0,
crossAxisExtent: 800.0,
)
);
// cache extent before(250.0) + viewport(600.0) + cache extent after(250.0)
expect(buildLog.length, 12);
expect(buildLog.min, 47);
expect(buildLog.max, 58);
buildLog.clear();
controller.jumpTo(4700.0);
await tester.pump();
expect(find.text('Item 46'), findsNothing);
expect(find.text('Item 47'), findsOneWidget);
expect(find.text('Item 52'), findsOneWidget);
expect(find.text('Item 53'), findsNothing);
expect(
sliverLayoutDimensions,
const SliverLayoutDimensions(
scrollOffset: 4700.0,
precedingScrollExtent: 0.0,
viewportMainAxisExtent: 600.0,
crossAxisExtent: 800.0,
)
);
// Only newly entered cached area items need to be loaded.
expect(buildLog.length, 3);
expect(buildLog.min, 44);
expect(buildLog.max, 46);
buildLog.clear();
controller.jumpTo(5300.0);
await tester.pump();
expect(find.text('Item 52'), findsNothing);
expect(find.text('Item 53'), findsOneWidget);
expect(find.text('Item 58'), findsOneWidget);
expect(find.text('Item 59'), findsNothing);
expect(
sliverLayoutDimensions,
const SliverLayoutDimensions(
scrollOffset: 5300.0,
precedingScrollExtent: 0.0,
viewportMainAxisExtent: 600.0,
crossAxisExtent: 800.0,
)
);
// Only newly entered cached area items need to be loaded.
expect(buildLog.length, 6);
expect(buildLog.min, 56);
expect(buildLog.max, 61);
});
testWidgets('itemExtent, prototypeItem and itemExtentBuilder conflicts test', (WidgetTester tester) async {
Object? error;
try {
await tester.pumpWidget(
ListView.builder(
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
return 100.0;
},
itemExtent: 100.0,
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
);
} catch (e) {
error = e;
}
expect(error, isNotNull);
error = null;
try {
await tester.pumpWidget(
ListView.builder(
itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
return 100.0;
},
prototypeItem: Container(),
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
);
} catch (e) {
error = e;
}
expect(error, isNotNull);
error = null;
try {
await tester.pumpWidget(
ListView.builder(
itemExtent: 100.0,
prototypeItem: Container(),
itemBuilder: (BuildContext context, int index) {
return Text('Item $index');
},
),
);
} catch (e) {
error = e;
}
expect(error, isNotNull);
});
}
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