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 @@ ...@@ -5,6 +5,7 @@
import 'dart:ui' show lerpDouble; import 'dart:ui' show lerpDouble;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'debug.dart'; import 'debug.dart';
...@@ -76,6 +77,7 @@ class ReorderableListView extends StatefulWidget { ...@@ -76,6 +77,7 @@ class ReorderableListView extends StatefulWidget {
this.onReorderStart, this.onReorderStart,
this.onReorderEnd, this.onReorderEnd,
this.itemExtent, this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem, this.prototypeItem,
this.proxyDecorator, this.proxyDecorator,
this.buildDefaultDragHandles = true, this.buildDefaultDragHandles = true,
...@@ -96,8 +98,10 @@ class ReorderableListView extends StatefulWidget { ...@@ -96,8 +98,10 @@ class ReorderableListView extends StatefulWidget {
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar, this.autoScrollerVelocityScalar,
}) : assert( }) : assert(
itemExtent == null || prototypeItem == null, (itemExtent == null && prototypeItem == null) ||
'You can only pass itemExtent or prototypeItem, not both', (itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
), ),
assert( assert(
children.every((Widget w) => w.key != null), children.every((Widget w) => w.key != null),
...@@ -142,6 +146,7 @@ class ReorderableListView extends StatefulWidget { ...@@ -142,6 +146,7 @@ class ReorderableListView extends StatefulWidget {
this.onReorderStart, this.onReorderStart,
this.onReorderEnd, this.onReorderEnd,
this.itemExtent, this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem, this.prototypeItem,
this.proxyDecorator, this.proxyDecorator,
this.buildDefaultDragHandles = true, this.buildDefaultDragHandles = true,
...@@ -163,8 +168,10 @@ class ReorderableListView extends StatefulWidget { ...@@ -163,8 +168,10 @@ class ReorderableListView extends StatefulWidget {
this.autoScrollerVelocityScalar, this.autoScrollerVelocityScalar,
}) : assert(itemCount >= 0), }) : assert(itemCount >= 0),
assert( assert(
itemExtent == null || prototypeItem == null, (itemExtent == null && prototypeItem == null) ||
'You can only pass itemExtent or prototypeItem, not both', (itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
); );
/// {@macro flutter.widgets.reorderable_list.itemBuilder} /// {@macro flutter.widgets.reorderable_list.itemBuilder}
...@@ -269,6 +276,9 @@ class ReorderableListView extends StatefulWidget { ...@@ -269,6 +276,9 @@ class ReorderableListView extends StatefulWidget {
/// {@macro flutter.widgets.list_view.itemExtent} /// {@macro flutter.widgets.list_view.itemExtent}
final double? itemExtent; final double? itemExtent;
/// {@macro flutter.widgets.list_view.itemExtentBuilder}
final ItemExtentBuilder? itemExtentBuilder;
/// {@macro flutter.widgets.list_view.prototypeItem} /// {@macro flutter.widgets.list_view.prototypeItem}
final Widget? prototypeItem; final Widget? prototypeItem;
...@@ -440,6 +450,7 @@ class _ReorderableListViewState extends State<ReorderableListView> { ...@@ -440,6 +450,7 @@ class _ReorderableListViewState extends State<ReorderableListView> {
sliver: SliverReorderableList( sliver: SliverReorderableList(
itemBuilder: _itemBuilder, itemBuilder: _itemBuilder,
itemExtent: widget.itemExtent, itemExtent: widget.itemExtent,
itemExtentBuilder: widget.itemExtentBuilder,
prototypeItem: widget.prototypeItem, prototypeItem: widget.prototypeItem,
itemCount: widget.itemCount, itemCount: widget.itemCount,
onReorder: widget.onReorder, onReorder: widget.onReorder,
......
...@@ -16,6 +16,71 @@ import 'viewport_offset.dart'; ...@@ -16,6 +16,71 @@ import 'viewport_offset.dart';
// CORE TYPES FOR SLIVERS // CORE TYPES FOR SLIVERS
// The RenderSliver base class and its helper types. // 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 /// The direction in which a sliver's contents are ordered, relative to the
/// scroll offset axis. /// scroll offset axis.
/// ///
...@@ -224,12 +289,13 @@ class SliverConstraints extends Constraints { ...@@ -224,12 +289,13 @@ class SliverConstraints extends Constraints {
/// {@macro flutter.rendering.ScrollDirection.sample} /// {@macro flutter.rendering.ScrollDirection.sample}
final ScrollDirection userScrollDirection; final ScrollDirection userScrollDirection;
/// {@template flutter.rendering.SliverConstraints.scrollOffset}
/// The scroll offset, in this sliver's coordinate system, that corresponds to /// The scroll offset, in this sliver's coordinate system, that corresponds to
/// the earliest visible part of this sliver in the [AxisDirection] if /// the earliest visible part of this sliver in the [AxisDirection] if
/// [growthDirection] is [GrowthDirection.forward] or in the opposite /// [SliverConstraints.growthDirection] is [GrowthDirection.forward] or in the opposite
/// [AxisDirection] direction if [growthDirection] is [GrowthDirection.reverse]. /// [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 /// is [GrowthDirection.forward], then scroll offset is the amount the top of
/// the sliver has been scrolled past the top of the viewport. /// the sliver has been scrolled past the top of the viewport.
/// ///
...@@ -240,7 +306,7 @@ class SliverConstraints extends Constraints { ...@@ -240,7 +306,7 @@ class SliverConstraints extends Constraints {
/// ///
/// For slivers whose top is not past the top of the viewport, the /// For slivers whose top is not past the top of the viewport, the
/// [scrollOffset] is `0` when [AxisDirection] is [AxisDirection.down] and /// [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 /// [scrollOffset] `0` includes all the slivers that are below the bottom of the
/// viewport. /// viewport.
/// ///
...@@ -249,9 +315,11 @@ class SliverConstraints extends Constraints { ...@@ -249,9 +315,11 @@ class SliverConstraints extends Constraints {
/// partially 'protrude in' from the bottom of the viewport. /// partially 'protrude in' from the bottom of the viewport.
/// ///
/// Whether this corresponds to the beginning or the end of the sliver's /// 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; final double scrollOffset;
/// {@template flutter.rendering.SliverConstraints.precedingScrollExtent}
/// The scroll distance that has been consumed by all [RenderSliver]s that /// The scroll distance that has been consumed by all [RenderSliver]s that
/// came before this [RenderSliver]. /// came before this [RenderSliver].
/// ///
...@@ -273,6 +341,7 @@ class SliverConstraints extends Constraints { ...@@ -273,6 +341,7 @@ class SliverConstraints extends Constraints {
/// content forever without reaching the end. For any [RenderSliver]s that /// content forever without reaching the end. For any [RenderSliver]s that
/// appear after the infinite [RenderSliver], the [precedingScrollExtent] will /// appear after the infinite [RenderSliver], the [precedingScrollExtent] will
/// be [double.infinity]. /// be [double.infinity].
/// {@endtemplate}
final double precedingScrollExtent; final double precedingScrollExtent;
/// The number of pixels from where the pixels corresponding to the /// The number of pixels from where the pixels corresponding to the
......
...@@ -21,6 +21,7 @@ import 'scrollable.dart'; ...@@ -21,6 +21,7 @@ import 'scrollable.dart';
import 'scrollable_helpers.dart'; import 'scrollable_helpers.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'sliver_prototype_extent_list.dart'; import 'sliver_prototype_extent_list.dart';
import 'sliver_varied_extent_list.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'transitions.dart'; import 'transitions.dart';
...@@ -118,6 +119,7 @@ class ReorderableList extends StatefulWidget { ...@@ -118,6 +119,7 @@ class ReorderableList extends StatefulWidget {
this.onReorderStart, this.onReorderStart,
this.onReorderEnd, this.onReorderEnd,
this.itemExtent, this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem, this.prototypeItem,
this.proxyDecorator, this.proxyDecorator,
this.padding, this.padding,
...@@ -135,10 +137,12 @@ class ReorderableList extends StatefulWidget { ...@@ -135,10 +137,12 @@ class ReorderableList extends StatefulWidget {
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.autoScrollerVelocityScalar, this.autoScrollerVelocityScalar,
}) : assert(itemCount >= 0), }) : assert(itemCount >= 0),
assert( assert(
itemExtent == null || prototypeItem == null, (itemExtent == null && prototypeItem == null) ||
'You can only pass itemExtent or prototypeItem, not both', (itemExtent == null && itemExtentBuilder == null) ||
); (prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
);
/// {@template flutter.widgets.reorderable_list.itemBuilder} /// {@template flutter.widgets.reorderable_list.itemBuilder}
/// Called, as needed, to build list item widgets. /// Called, as needed, to build list item widgets.
...@@ -253,6 +257,9 @@ class ReorderableList extends StatefulWidget { ...@@ -253,6 +257,9 @@ class ReorderableList extends StatefulWidget {
/// {@macro flutter.widgets.list_view.itemExtent} /// {@macro flutter.widgets.list_view.itemExtent}
final double? itemExtent; final double? itemExtent;
/// {@macro flutter.widgets.list_view.itemExtentBuilder}
final ItemExtentBuilder? itemExtentBuilder;
/// {@macro flutter.widgets.list_view.prototypeItem} /// {@macro flutter.widgets.list_view.prototypeItem}
final Widget? prototypeItem; final Widget? prototypeItem;
...@@ -450,14 +457,17 @@ class SliverReorderableList extends StatefulWidget { ...@@ -450,14 +457,17 @@ class SliverReorderableList extends StatefulWidget {
this.onReorderStart, this.onReorderStart,
this.onReorderEnd, this.onReorderEnd,
this.itemExtent, this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem, this.prototypeItem,
this.proxyDecorator, this.proxyDecorator,
double? autoScrollerVelocityScalar, double? autoScrollerVelocityScalar,
}) : autoScrollerVelocityScalar = autoScrollerVelocityScalar ?? _kDefaultAutoScrollVelocityScalar, }) : autoScrollerVelocityScalar = autoScrollerVelocityScalar ?? _kDefaultAutoScrollVelocityScalar,
assert(itemCount >= 0), assert(itemCount >= 0),
assert( assert(
itemExtent == null || prototypeItem == null, (itemExtent == null && prototypeItem == null) ||
'You can only pass itemExtent or prototypeItem, not both', (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. // An eyeballed value for a smooth scrolling experience.
...@@ -487,6 +497,9 @@ class SliverReorderableList extends StatefulWidget { ...@@ -487,6 +497,9 @@ class SliverReorderableList extends StatefulWidget {
/// {@macro flutter.widgets.list_view.itemExtent} /// {@macro flutter.widgets.list_view.itemExtent}
final double? itemExtent; final double? itemExtent;
/// {@macro flutter.widgets.list_view.itemExtentBuilder}
final ItemExtentBuilder? itemExtentBuilder;
/// {@macro flutter.widgets.list_view.prototypeItem} /// {@macro flutter.widgets.list_view.prototypeItem}
final Widget? prototypeItem; final Widget? prototypeItem;
...@@ -1036,6 +1049,11 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke ...@@ -1036,6 +1049,11 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
delegate: childrenDelegate, delegate: childrenDelegate,
itemExtent: widget.itemExtent!, itemExtent: widget.itemExtent!,
); );
} else if (widget.itemExtentBuilder != null) {
return SliverVariedExtentList(
delegate: childrenDelegate,
itemExtentBuilder: widget.itemExtentBuilder!,
);
} else if (widget.prototypeItem != null) { } else if (widget.prototypeItem != null) {
return SliverPrototypeExtentList( return SliverPrototypeExtentList(
delegate: childrenDelegate, delegate: childrenDelegate,
......
...@@ -24,6 +24,7 @@ import 'scrollable.dart'; ...@@ -24,6 +24,7 @@ import 'scrollable.dart';
import 'scrollable_helpers.dart'; import 'scrollable_helpers.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'sliver_prototype_extent_list.dart'; import 'sliver_prototype_extent_list.dart';
import 'sliver_varied_extent_list.dart';
import 'viewport.dart'; import 'viewport.dart';
// Examples can assume: // Examples can assume:
...@@ -1230,6 +1231,7 @@ class ListView extends BoxScrollView { ...@@ -1230,6 +1231,7 @@ class ListView extends BoxScrollView {
super.shrinkWrap, super.shrinkWrap,
super.padding, super.padding,
this.itemExtent, this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem, this.prototypeItem,
bool addAutomaticKeepAlives = true, bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true, bool addRepaintBoundaries = true,
...@@ -1242,8 +1244,10 @@ class ListView extends BoxScrollView { ...@@ -1242,8 +1244,10 @@ class ListView extends BoxScrollView {
super.restorationId, super.restorationId,
super.clipBehavior, super.clipBehavior,
}) : assert( }) : assert(
itemExtent == null || prototypeItem == null, (itemExtent == null && prototypeItem == null) ||
'You can only pass itemExtent or prototypeItem, not both.', (itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
), ),
childrenDelegate = SliverChildListDelegate( childrenDelegate = SliverChildListDelegate(
children, children,
...@@ -1303,6 +1307,7 @@ class ListView extends BoxScrollView { ...@@ -1303,6 +1307,7 @@ class ListView extends BoxScrollView {
super.shrinkWrap, super.shrinkWrap,
super.padding, super.padding,
this.itemExtent, this.itemExtent,
this.itemExtentBuilder,
this.prototypeItem, this.prototypeItem,
required NullableIndexedWidgetBuilder itemBuilder, required NullableIndexedWidgetBuilder itemBuilder,
ChildIndexGetter? findChildIndexCallback, ChildIndexGetter? findChildIndexCallback,
...@@ -1319,8 +1324,10 @@ class ListView extends BoxScrollView { ...@@ -1319,8 +1324,10 @@ class ListView extends BoxScrollView {
}) : assert(itemCount == null || itemCount >= 0), }) : assert(itemCount == null || itemCount >= 0),
assert(semanticChildCount == null || semanticChildCount <= itemCount!), assert(semanticChildCount == null || semanticChildCount <= itemCount!),
assert( assert(
itemExtent == null || prototypeItem == null, (itemExtent == null && prototypeItem == null) ||
'You can only pass itemExtent or prototypeItem, not both.', (itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
), ),
childrenDelegate = SliverChildBuilderDelegate( childrenDelegate = SliverChildBuilderDelegate(
itemBuilder, itemBuilder,
...@@ -1408,6 +1415,7 @@ class ListView extends BoxScrollView { ...@@ -1408,6 +1415,7 @@ class ListView extends BoxScrollView {
super.clipBehavior, super.clipBehavior,
}) : assert(itemCount >= 0), }) : assert(itemCount >= 0),
itemExtent = null, itemExtent = null,
itemExtentBuilder = null,
prototypeItem = null, prototypeItem = null,
childrenDelegate = SliverChildBuilderDelegate( childrenDelegate = SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
...@@ -1528,6 +1536,7 @@ class ListView extends BoxScrollView { ...@@ -1528,6 +1536,7 @@ class ListView extends BoxScrollView {
super.padding, super.padding,
this.itemExtent, this.itemExtent,
this.prototypeItem, this.prototypeItem,
this.itemExtentBuilder,
required this.childrenDelegate, required this.childrenDelegate,
super.cacheExtent, super.cacheExtent,
super.semanticChildCount, super.semanticChildCount,
...@@ -1536,8 +1545,10 @@ class ListView extends BoxScrollView { ...@@ -1536,8 +1545,10 @@ class ListView extends BoxScrollView {
super.restorationId, super.restorationId,
super.clipBehavior, super.clipBehavior,
}) : assert( }) : assert(
itemExtent == null || prototypeItem == null, (itemExtent == null && prototypeItem == null) ||
'You can only pass itemExtent or prototypeItem, not both', (itemExtent == null && itemExtentBuilder == null) ||
(prototypeItem == null && itemExtentBuilder == null),
'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
); );
/// {@template flutter.widgets.list_view.itemExtent} /// {@template flutter.widgets.list_view.itemExtent}
...@@ -1556,9 +1567,38 @@ class ListView extends BoxScrollView { ...@@ -1556,9 +1567,38 @@ class ListView extends BoxScrollView {
/// extent along the main axis. /// extent along the main axis.
/// * The [prototypeItem] property, which allows forcing the children's /// * The [prototypeItem] property, which allows forcing the children's
/// extent to be the same as the given widget. /// 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} /// {@endtemplate}
final double? itemExtent; 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} /// {@template flutter.widgets.list_view.prototypeItem}
/// If non-null, forces the children to have the same extent as the given /// If non-null, forces the children to have the same extent as the given
/// widget in the scroll direction. /// widget in the scroll direction.
...@@ -1575,6 +1615,8 @@ class ListView extends BoxScrollView { ...@@ -1575,6 +1615,8 @@ class ListView extends BoxScrollView {
/// extent as a prototype item along the main axis. /// extent as a prototype item along the main axis.
/// * The [itemExtent] property, which allows forcing the children's extent /// * The [itemExtent] property, which allows forcing the children's extent
/// to a given value. /// to a given value.
/// * The [itemExtentBuilder] property, which allows forcing the children's
/// extent to be the value returned by the callback.
/// {@endtemplate} /// {@endtemplate}
final Widget? prototypeItem; final Widget? prototypeItem;
...@@ -1593,6 +1635,11 @@ class ListView extends BoxScrollView { ...@@ -1593,6 +1635,11 @@ class ListView extends BoxScrollView {
delegate: childrenDelegate, delegate: childrenDelegate,
itemExtent: itemExtent!, itemExtent: itemExtent!,
); );
} else if (itemExtentBuilder != null) {
return SliverVariedExtentList(
delegate: childrenDelegate,
itemExtentBuilder: itemExtentBuilder!,
);
} else if (prototypeItem != null) { } else if (prototypeItem != null) {
return SliverPrototypeExtentList( return SliverPrototypeExtentList(
delegate: childrenDelegate, 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'; ...@@ -136,6 +136,7 @@ export 'src/widgets/sliver_fill.dart';
export 'src/widgets/sliver_layout_builder.dart'; export 'src/widgets/sliver_layout_builder.dart';
export 'src/widgets/sliver_persistent_header.dart'; export 'src/widgets/sliver_persistent_header.dart';
export 'src/widgets/sliver_prototype_extent_list.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/slotted_render_object_widget.dart';
export 'src/widgets/snapshot_widget.dart'; export 'src/widgets/snapshot_widget.dart';
export 'src/widgets/spacer.dart'; export 'src/widgets/spacer.dart';
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -776,4 +777,189 @@ void main() { ...@@ -776,4 +777,189 @@ void main() {
final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first; final RenderViewport renderObject = tester.allRenderObjects.whereType<RenderViewport>().first;
expect(renderObject.clipBehavior, equals(Clip.antiAlias)); 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