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
......
......@@ -10,16 +10,17 @@ import 'box.dart';
import 'sliver.dart';
import 'sliver_multi_box_adaptor.dart';
/// A sliver that contains multiple box children that have the same extent in
/// A sliver that contains multiple box children that have the explicit extent in
/// the main axis.
///
/// [RenderSliverFixedExtentBoxAdaptor] places its children in a linear array
/// along the main axis. Each child is forced to have the [itemExtent] in the
/// main axis and the [SliverConstraints.crossAxisExtent] in the cross axis.
/// along the main axis. Each child is forced to have the returned value of [itemExtentBuilder]
/// when the [itemExtentBuilder] is non-null or the [itemExtent] when [itemExtentBuilder]
/// is null in the main axis and the [SliverConstraints.crossAxisExtent] in the cross axis.
///
/// Subclasses should override [itemExtent] to control the size of the children
/// in the main axis. For a concrete subclass with a configurable [itemExtent],
/// see [RenderSliverFixedExtentList].
/// Subclasses should override [itemExtent] or [itemExtentBuilder] to control
/// the size of the children in the main axis. For a concrete subclass with a
/// configurable [itemExtent], see [RenderSliverFixedExtentList] or [RenderSliverVariedExtentList].
///
/// [RenderSliverFixedExtentBoxAdaptor] is more efficient than
/// [RenderSliverList] because [RenderSliverFixedExtentBoxAdaptor] does not need
......@@ -44,27 +45,47 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
});
/// The main-axis extent of each item.
double get itemExtent;
///
/// If this is non-null, the [itemExtentBuilder] must be null.
/// If this is null, the [itemExtentBuilder] must be non-null.
double? get itemExtent;
/// The main-axis extent builder of each item.
///
/// If this is non-null, the [itemExtent] must be null.
/// If this is null, the [itemExtent] must be non-null.
ItemExtentBuilder? get itemExtentBuilder => null;
/// The layout offset for the child with the given index.
///
/// This function is given the [itemExtent] as an argument to avoid
/// recomputing [itemExtent] repeatedly during layout.
/// This function uses the returned value of [itemExtentBuilder] or the [itemExtent]
/// as an argument to avoid recomputing item size repeatedly during layout.
///
/// By default, places the children in order, without gaps, starting from
/// layout offset zero.
@protected
double indexToLayoutOffset(double itemExtent, int index) => itemExtent * index;
double indexToLayoutOffset(double itemExtent, int index) {
if (itemExtentBuilder == null) {
return itemExtent * index;
} else {
double offset = 0.0;
for (int i = 0; i < index; i++) {
offset += itemExtentBuilder!(i, _currentLayoutDimensions);
}
return offset;
}
}
/// The minimum child index that is visible at the given scroll offset.
///
/// This function is given the [itemExtent] as an argument to avoid
/// recomputing [itemExtent] repeatedly during layout.
/// This function uses the returned value of [itemExtentBuilder] or the [itemExtent]
/// as an argument to avoid recomputing item size repeatedly during layout.
///
/// By default, returns a value consistent with the children being placed in
/// order, without gaps, starting from layout offset zero.
@protected
int getMinChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
if (itemExtentBuilder == null) {
if (itemExtent > 0.0) {
final double actual = scrollOffset / itemExtent;
final int round = actual.round();
......@@ -74,17 +95,21 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
return actual.floor();
}
return 0;
} else {
return _getChildIndexForScrollOffset(scrollOffset, itemExtentBuilder!);
}
}
/// The maximum child index that is visible at the given scroll offset.
///
/// This function is given the [itemExtent] as an argument to avoid
/// recomputing [itemExtent] repeatedly during layout.
/// This function uses the returned value of [itemExtentBuilder] or the [itemExtent]
/// as an argument to avoid recomputing item size repeatedly during layout.
///
/// By default, returns a value consistent with the children being placed in
/// order, without gaps, starting from layout offset zero.
@protected
int getMaxChildIndexForScrollOffset(double scrollOffset, double itemExtent) {
if (itemExtentBuilder == null) {
if (itemExtent > 0.0) {
final double actual = scrollOffset / itemExtent - 1;
final int round = actual.round();
......@@ -94,6 +119,9 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
return math.max(0, actual.ceil());
}
return 0;
} else {
return _getChildIndexForScrollOffset(scrollOffset, itemExtentBuilder!);
}
}
/// Called to estimate the total scrollable extents of this object.
......@@ -138,8 +166,10 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
/// [childManager] returns an infinite number of children for positive
/// indices.
///
/// By default, multiplies the [itemExtent] by the number of children reported
/// by [RenderSliverBoxChildManager.childCount].
/// If [itemExtentBuilder] is null, multiplies the [itemExtent] by the number
/// of children reported by [RenderSliverBoxChildManager.childCount].
/// If [itemExtentBuilder] is non-null, sum the extents of the first
/// [RenderSliverBoxChildManager.childCount] children.
///
/// See also:
///
......@@ -147,7 +177,15 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
/// values.
@protected
double computeMaxScrollOffset(SliverConstraints constraints, double itemExtent) {
if (itemExtentBuilder == null) {
return childManager.childCount * itemExtent;
} else {
double offset = 0.0;
for (int i = 0; i < childManager.childCount; i++) {
offset += itemExtentBuilder!(i, _currentLayoutDimensions);
}
return offset;
}
}
int _calculateLeadingGarbage(int firstIndex) {
......@@ -170,28 +208,61 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
return trailingGarbage;
}
int _getChildIndexForScrollOffset(double scrollOffset, ItemExtentBuilder callback) {
if (scrollOffset == 0.0) {
return 0;
}
double position = 0.0;
int index = 0;
while (position < scrollOffset) {
position += callback(index, _currentLayoutDimensions);
++index;
}
return index - 1;
}
BoxConstraints _getChildConstraints(int index) {
double extent;
if (itemExtentBuilder == null) {
extent = itemExtent!;
} else {
extent = itemExtentBuilder!(index, _currentLayoutDimensions);
}
return constraints.asBoxConstraints(
minExtent: extent,
maxExtent: extent,
);
}
late SliverLayoutDimensions _currentLayoutDimensions;
@override
void performLayout() {
assert((itemExtent != null && itemExtentBuilder == null) ||
(itemExtent == null && itemExtentBuilder != null));
assert(itemExtentBuilder != null || (itemExtent!.isFinite && itemExtent! >= 0));
final SliverConstraints constraints = this.constraints;
childManager.didStartLayout();
childManager.setDidUnderflow(false);
final double itemExtent = this.itemExtent;
final double itemFixedExtent = itemExtent ?? 0;
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingExtent;
final BoxConstraints childConstraints = constraints.asBoxConstraints(
minExtent: itemExtent,
maxExtent: itemExtent,
_currentLayoutDimensions = SliverLayoutDimensions(
scrollOffset: constraints.scrollOffset,
precedingScrollExtent: constraints.precedingScrollExtent,
viewportMainAxisExtent: constraints.viewportMainAxisExtent,
crossAxisExtent: constraints.crossAxisExtent
);
final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, itemExtent);
final int firstIndex = getMinChildIndexForScrollOffset(scrollOffset, itemFixedExtent);
final int? targetLastIndex = targetEndScrollOffset.isFinite ?
getMaxChildIndexForScrollOffset(targetEndScrollOffset, itemExtent) : null;
getMaxChildIndexForScrollOffset(targetEndScrollOffset, itemFixedExtent) : null;
if (firstChild != null) {
final int leadingGarbage = _calculateLeadingGarbage(firstIndex);
......@@ -202,13 +273,13 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
}
if (firstChild == null) {
if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) {
if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemFixedExtent, firstIndex))) {
// There are either no children, or we are past the end of all our children.
final double max;
if (firstIndex <= 0) {
max = 0.0;
} else {
max = computeMaxScrollOffset(constraints, itemExtent);
max = computeMaxScrollOffset(constraints, itemFixedExtent);
}
geometry = SliverGeometry(
scrollExtent: max,
......@@ -222,24 +293,24 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
RenderBox? trailingChildWithLayout;
for (int index = indexOf(firstChild!) - 1; index >= firstIndex; --index) {
final RenderBox? child = insertAndLayoutLeadingChild(childConstraints);
final RenderBox? child = insertAndLayoutLeadingChild(_getChildConstraints(index));
if (child == null) {
// Items before the previously first child are no longer present.
// Reset the scroll offset to offset all items prior and up to the
// missing item. Let parent re-layout everything.
geometry = SliverGeometry(scrollOffsetCorrection: index * itemExtent);
geometry = SliverGeometry(scrollOffsetCorrection: indexToLayoutOffset(itemFixedExtent, index));
return;
}
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = indexToLayoutOffset(itemExtent, index);
childParentData.layoutOffset = indexToLayoutOffset(itemFixedExtent, index);
assert(childParentData.index == index);
trailingChildWithLayout ??= child;
}
if (trailingChildWithLayout == null) {
firstChild!.layout(childConstraints);
firstChild!.layout(_getChildConstraints(indexOf(firstChild!)));
final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = indexToLayoutOffset(itemExtent, firstIndex);
childParentData.layoutOffset = indexToLayoutOffset(itemFixedExtent, firstIndex);
trailingChildWithLayout = firstChild;
}
......@@ -247,24 +318,24 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
for (int index = indexOf(trailingChildWithLayout!) + 1; targetLastIndex == null || index <= targetLastIndex; ++index) {
RenderBox? child = childAfter(trailingChildWithLayout!);
if (child == null || indexOf(child) != index) {
child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout);
child = insertAndLayoutChild(_getChildConstraints(index), after: trailingChildWithLayout);
if (child == null) {
// We have run out of children.
estimatedMaxScrollOffset = index * itemExtent;
estimatedMaxScrollOffset = indexToLayoutOffset(itemFixedExtent, index);
break;
}
} else {
child.layout(childConstraints);
child.layout(_getChildConstraints(index));
}
trailingChildWithLayout = child;
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
assert(childParentData.index == index);
childParentData.layoutOffset = indexToLayoutOffset(itemExtent, childParentData.index!);
childParentData.layoutOffset = indexToLayoutOffset(itemFixedExtent, childParentData.index!);
}
final int lastIndex = indexOf(lastChild!);
final double leadingScrollOffset = indexToLayoutOffset(itemExtent, firstIndex);
final double trailingScrollOffset = indexToLayoutOffset(itemExtent, lastIndex + 1);
final double leadingScrollOffset = indexToLayoutOffset(itemFixedExtent, firstIndex);
final double trailingScrollOffset = indexToLayoutOffset(itemFixedExtent, lastIndex + 1);
assert(firstIndex == 0 || childScrollOffset(firstChild!)! - scrollOffset <= precisionErrorTolerance);
assert(debugAssertChildListIsNonEmptyAndContiguous());
......@@ -296,7 +367,8 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
final int? targetLastIndexForPaint = targetEndScrollOffsetForPaint.isFinite ?
getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint, itemExtent) : null;
getMaxChildIndexForScrollOffset(targetEndScrollOffsetForPaint, itemFixedExtent) : null;
geometry = SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
......
......@@ -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,
......@@ -136,8 +138,10 @@ class ReorderableList 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.',
);
/// {@template flutter.widgets.reorderable_list.itemBuilder}
......@@ -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