feat(ui): update scrollable_positioned_list with the latest changes.

Signed-off-by: xsahil03x <[email protected]>
This commit is contained in:
Sahil Kumar
2023-05-03 15:47:53 +05:30
committed by xsahil03x
parent c50a435515
commit b4413be47d
14 changed files with 2333 additions and 397 deletions
@@ -11,6 +11,7 @@ import 'package:stream_chat_flutter/scrollable_positioned_list/src/indexed_key.d
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_listener.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart';
/// A list of widgets similar to [ListView], except scroll control
/// and position reporting is based on index rather than pixel offset.
@@ -35,28 +36,20 @@ class PositionedList extends StatefulWidget {
this.alignment = 0,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.shrinkWrap = false,
this.physics,
this.padding,
this.cacheExtent,
this.semanticChildCount,
this.findChildIndexCallback,
this.addSemanticIndexes = true,
this.addRepaintBoundaries = true,
this.addAutomaticKeepAlives = true,
this.keyboardDismissBehavior,
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
'positionedIndex cannot be 0 and must be smaller than itemCount');
/// Called to find the new index of a child based on its key in case of
/// reordering.
///
/// If not provided, a child widget may not map to its existing [RenderObject]
/// when the order in which children are returned from [builder] changes.
/// This may result in state-loss.
///
/// This callback should take an input [Key], and it should return the
/// index of the child element with that associated key, or null if not found.
final ChildIndexGetter? findChildIndexCallback;
this.findChildIndexCallback,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
}) : assert(
(positionedIndex == 0) || (positionedIndex < itemCount),
'positionedIndex must be 0 or a value less than itemCount',
);
/// Number of items the [itemBuilder] can produce.
final int itemCount;
@@ -98,6 +91,15 @@ class PositionedList extends StatefulWidget {
/// See [ScrollView.reverse].
final bool reverse;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// Defaults to false.
///
/// See [ScrollView.shrinkWrap].
final bool shrinkWrap;
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
@@ -132,9 +134,22 @@ class PositionedList extends StatefulWidget {
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
final bool addAutomaticKeepAlives;
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
/// Called to find the new index of a child based on its key in case of reordering.
///
/// If not provided, a child widget may not map to its existing [RenderObject]
/// when the order of children returned from the children builder changes.
/// This may result in state-loss.
///
/// This callback should take an input [Key], and it should return the
/// index of the child element with that associated key, or null if not found.
///
/// See [SliverChildBuilderDelegate.findChildIndexCallback].
final ChildIndexGetter? findChildIndexCallback;
/// Defines how this [ScrollView] will dismiss the keyboard automatically.
///
/// See [ScrollView.keyboardDismissBehavior].
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
@override
State<StatefulWidget> createState() => _PositionedListState();
@@ -175,12 +190,13 @@ class _PositionedListState extends State<PositionedList> {
anchor: widget.alignment,
center: _centerKey,
controller: scrollController,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: widget.cacheExtent,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
semanticChildCount: widget.semanticChildCount ?? widget.itemCount,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
slivers: <Widget>[
if (widget.positionedIndex > 0)
SliverPadding(
@@ -196,9 +212,9 @@ class _PositionedListState extends State<PositionedList> {
? widget.positionedIndex
: widget.positionedIndex * 2,
addSemanticIndexes: false,
findChildIndexCallback: widget.findChildIndexCallback,
addRepaintBoundaries: widget.addRepaintBoundaries,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
findChildIndexCallback: widget.findChildIndexCallback,
),
),
),
@@ -213,10 +229,10 @@ class _PositionedListState extends State<PositionedList> {
index + widget.positionedIndex * 2,
),
childCount: widget.itemCount != 0 ? 1 : 0,
findChildIndexCallback: widget.findChildIndexCallback,
addSemanticIndexes: false,
addRepaintBoundaries: widget.addRepaintBoundaries,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
findChildIndexCallback: widget.findChildIndexCallback,
),
),
),
@@ -234,10 +250,10 @@ class _PositionedListState extends State<PositionedList> {
childCount: widget.separatorBuilder == null
? widget.itemCount - widget.positionedIndex - 1
: 2 * (widget.itemCount - widget.positionedIndex - 1),
findChildIndexCallback: widget.findChildIndexCallback,
addSemanticIndexes: false,
addRepaintBoundaries: widget.addRepaintBoundaries,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
findChildIndexCallback: widget.findChildIndexCallback,
),
),
),
@@ -319,25 +335,33 @@ class _PositionedListState extends State<PositionedList> {
if (!updateScheduled) {
updateScheduled = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (registeredElements.value == null) {
final elements = registeredElements.value;
if (elements == null) {
updateScheduled = false;
return;
}
final positions = <ItemPosition>[];
RenderViewport? viewport;
for (final element in registeredElements.value!) {
final box = element.renderObject as RenderBox?;
viewport ??= RenderAbstractViewport.of(box) as RenderViewport?;
if (viewport == null || box == null) {
break;
RenderViewportBase? viewport;
for (final element in elements) {
final box = element.renderObject! as RenderBox;
viewport ??= RenderAbstractViewport.of(box) as RenderViewportBase?;
var anchor = 0.0;
if (viewport is RenderViewport) {
anchor = viewport.anchor;
}
final key = element.widget.key as IndexedKey;
if (viewport is CustomRenderViewport) {
anchor = viewport.anchor;
}
final key = element.widget.key! as IndexedKey;
// Skip this element if `box` has never been laid out.
if (!box.hasSize) continue;
if (widget.scrollDirection == Axis.vertical) {
final reveal = viewport.getOffsetToReveal(box, 0).offset;
final reveal = viewport!.getOffsetToReveal(box, 0).offset;
if (!reveal.isFinite) continue;
final itemOffset = reveal -
viewport.offset.pixels +
viewport.anchor * viewport.size.height;
final itemOffset =
reveal - viewport.offset.pixels + anchor * viewport.size.height;
positions.add(ItemPosition(
index: key.index,
itemLeadingEdge: itemOffset.round() /
@@ -348,6 +372,7 @@ class _PositionedListState extends State<PositionedList> {
} else {
final itemOffset =
box.localToGlobal(Offset.zero, ancestor: viewport).dx;
if (!itemOffset.isFinite) continue;
positions.add(ItemPosition(
index: key.index,
itemLeadingEdge: (widget.reverse
@@ -5,13 +5,14 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/viewport.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart';
/// {@template custom_scroll_view}
/// A version of [CustomScrollView] that does not constrict the extents
/// {@template unbounded_custom_scroll_view}
/// A version of [CustomScrollView] that allows does not constrict the extents
/// to be within 0 and 1. See [CustomScrollView] for more information.
/// {@endtemplate}
class UnboundedCustomScrollView extends CustomScrollView {
/// {@macro custom_scroll_view}
/// {@macro unbounded_custom_scroll_view}
const UnboundedCustomScrollView({
super.key,
super.scrollDirection,
@@ -19,19 +20,19 @@ class UnboundedCustomScrollView extends CustomScrollView {
super.controller,
super.primary,
super.physics,
super.shrinkWrap,
bool shrinkWrap = false,
super.center,
double anchor = 0.0,
super.cacheExtent,
super.slivers,
super.semanticChildCount,
super.dragStartBehavior,
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
}) : _anchor = anchor,
super(
keyboardDismissBehavior: keyboardDismissBehavior ??
ScrollViewKeyboardDismissBehavior.manual,
);
super.keyboardDismissBehavior,
}) : _shrinkWrap = shrinkWrap,
_anchor = anchor,
super(shrinkWrap: false);
final bool _shrinkWrap;
// [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so
// we need our own version.
@@ -49,11 +50,14 @@ class UnboundedCustomScrollView extends CustomScrollView {
AxisDirection axisDirection,
List<Widget> slivers,
) {
if (shrinkWrap) {
return ShrinkWrappingViewport(
if (_shrinkWrap) {
return CustomShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
);
}
return UnboundedViewport(
@@ -37,6 +37,7 @@ class ScrollablePositionedList extends StatefulWidget {
required this.itemBuilder,
super.key,
this.itemScrollController,
this.shrinkWrap = false,
ItemPositionsListener? itemPositionsListener,
this.initialScrollIndex = 0,
this.initialAlignment = 0,
@@ -50,7 +51,7 @@ class ScrollablePositionedList extends StatefulWidget {
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
this.keyboardDismissBehavior,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
separatorBuilder = null;
@@ -61,6 +62,7 @@ class ScrollablePositionedList extends StatefulWidget {
required this.itemBuilder,
required IndexedWidgetBuilder this.separatorBuilder,
super.key,
this.shrinkWrap = false,
this.itemScrollController,
ItemPositionsListener? itemPositionsListener,
this.initialScrollIndex = 0,
@@ -75,24 +77,9 @@ class ScrollablePositionedList extends StatefulWidget {
this.addRepaintBoundaries = true,
this.minCacheExtent,
this.findChildIndexCallback,
this.keyboardDismissBehavior,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?;
/// Called to find the new index of a child based on its key in case of
/// reordering.
///
/// If not provided, a child widget may not map to its existing [RenderObject]
/// when the order in which children are returned from [builder] changes.
/// This may result in state-loss.
///
/// This callback should take an input [Key], and it should return the
/// index of the child element with that associated key, or null if not found.
final ChildIndexGetter? findChildIndexCallback;
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
/// dismiss the keyboard automatically.
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
/// Number of items the [itemBuilder] can produce.
final int itemCount;
@@ -131,6 +118,15 @@ class ScrollablePositionedList extends StatefulWidget {
/// See [ScrollView.reverse].
final bool reverse;
/// {@template flutter.widgets.scroll_view.shrinkWrap}
/// Whether the extent of the scroll view in the [scrollDirection] should be
/// determined by the contents being viewed.
///
/// Defaults to false.
///
/// See [ScrollView.shrinkWrap].
final bool shrinkWrap;
/// How the scroll view should respond to user input.
///
/// For example, determines how the scroll view continues to animate after the
@@ -171,6 +167,23 @@ class ScrollablePositionedList extends StatefulWidget {
/// cache extent.
final double? minCacheExtent;
/// Called to find the new index of a child based on its key in case of reordering.
///
/// If not provided, a child widget may not map to its existing [RenderObject]
/// when the order of children returned from the children builder changes.
/// This may result in state-loss.
///
/// This callback should take an input [Key], and it should return the
/// index of the child element with that associated key, or null if not found.
///
/// See [SliverChildBuilderDelegate.findChildIndexCallback].
final ChildIndexGetter? findChildIndexCallback;
/// Defines how this [ScrollView] will dismiss the keyboard automatically.
///
/// See [ScrollView.keyboardDismissBehavior].
final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
@override
State<StatefulWidget> createState() => _ScrollablePositionedListState();
}
@@ -233,11 +246,15 @@ class ItemScrollController {
Curve curve = Curves.linear,
List<double> opacityAnimationWeights = const [40, 20, 40],
}) {
assert(_scrollableListState != null, '_scrollableListState cannot be null');
assert(opacityAnimationWeights.length == 3,
'opacityAnimationWeights.length is not equal to 3');
assert(duration > Duration.zero,
'duration needs to be bigger than Duration.zero');
assert(
_scrollableListState != null,
'''ScrollController must be attached to a ScrollablePositionedList to scroll.''',
);
assert(
opacityAnimationWeights.length == 3,
'opacityAnimationWeights must have exactly three elements.',
);
assert(duration > Duration.zero, 'Duration must be greater than zero.');
return _scrollableListState!._scrollTo(
index: index,
alignment: alignment,
@@ -249,7 +266,9 @@ class ItemScrollController {
void _attach(_ScrollablePositionedListState scrollableListState) {
assert(
_scrollableListState == null, '_scrollableListState needs to be null');
_scrollableListState == null,
'''ScrollController must not be attached to multiple ScrollablePositionedLists.''',
);
_scrollableListState = scrollableListState;
}
@@ -273,11 +292,12 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
bool _isTransitioning = false;
AnimationController? _animationController;
@override
void initState() {
super.initState();
final ItemPosition? initialPosition =
PageStorage.of(context).readState(context);
final initialPosition = PageStorage.of(context).readState(context);
primary
..target = initialPosition?.index ?? widget.initialScrollIndex
..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment;
@@ -301,6 +321,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
.removeListener(_updatePositions);
secondary.itemPositionsNotifier.itemPositions
.removeListener(_updatePositions);
_animationController?.dispose();
super.dispose();
}
@@ -329,84 +350,90 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
}
@override
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, constraints) {
final cacheExtent = _cacheExtent(constraints);
return GestureDetector(
onPanDown: (_) => _stopScroll(canceled: true),
excludeFromSemantics: true,
child: Stack(
children: <Widget>[
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final cacheExtent = _cacheExtent(constraints);
return GestureDetector(
onPanDown: (_) => _stopScroll(canceled: true),
excludeFromSemantics: true,
child: Stack(
children: <Widget>[
PostMountCallback(
key: primary.key,
callback: startAnimationCallback,
child: FadeTransition(
opacity: ReverseAnimation(opacity),
child: NotificationListener<ScrollNotification>(
onNotification: (_) => _isTransitioning,
child: PositionedList(
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
positionedIndex: primary.target,
controller: primary.scrollController,
itemPositionsNotifier: primary.itemPositionsNotifier,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: cacheExtent,
alignment: primary.alignment,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
addSemanticIndexes: widget.addSemanticIndexes,
semanticChildCount: widget.semanticChildCount,
padding: widget.padding,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
findChildIndexCallback: widget.findChildIndexCallback,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
),
),
),
),
if (_isTransitioning)
PostMountCallback(
key: primary.key,
key: secondary.key,
callback: startAnimationCallback,
child: FadeTransition(
opacity: ReverseAnimation(opacity),
opacity: opacity,
child: NotificationListener<ScrollNotification>(
onNotification: (_) => _isTransitioning,
onNotification: (_) => false,
child: PositionedList(
keyboardDismissBehavior: widget.keyboardDismissBehavior,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
positionedIndex: primary.target,
controller: primary.scrollController,
itemPositionsNotifier: primary.itemPositionsNotifier,
itemPositionsNotifier: secondary.itemPositionsNotifier,
positionedIndex: secondary.target,
controller: secondary.scrollController,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: cacheExtent,
alignment: primary.alignment,
alignment: secondary.alignment,
physics: widget.physics,
shrinkWrap: widget.shrinkWrap,
addSemanticIndexes: widget.addSemanticIndexes,
semanticChildCount: widget.semanticChildCount,
padding: widget.padding,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
findChildIndexCallback: widget.findChildIndexCallback,
keyboardDismissBehavior: widget.keyboardDismissBehavior,
),
),
),
),
if (_isTransitioning)
PostMountCallback(
key: secondary.key,
callback: startAnimationCallback,
child: FadeTransition(
opacity: opacity,
child: NotificationListener<ScrollNotification>(
onNotification: (_) => false,
child: PositionedList(
keyboardDismissBehavior:
widget.keyboardDismissBehavior,
itemBuilder: widget.itemBuilder,
separatorBuilder: widget.separatorBuilder,
itemCount: widget.itemCount,
itemPositionsNotifier:
secondary.itemPositionsNotifier,
positionedIndex: secondary.target,
controller: secondary.scrollController,
scrollDirection: widget.scrollDirection,
reverse: widget.reverse,
cacheExtent: cacheExtent,
alignment: secondary.alignment,
physics: widget.physics,
addSemanticIndexes: widget.addSemanticIndexes,
semanticChildCount: widget.semanticChildCount,
padding: widget.padding,
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
addRepaintBoundaries: widget.addRepaintBoundaries,
),
),
),
),
],
),
);
},
);
],
),
);
},
);
}
double _cacheExtent(BoxConstraints constraints) => max(
constraints.maxHeight * _screenScrollCount,
(widget.scrollDirection == Axis.vertical
? constraints.maxHeight
: constraints.maxWidth) *
_screenScrollCount,
widget.minCacheExtent ?? 0,
);
@@ -434,16 +461,19 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
index = widget.itemCount - 1;
}
if (_isTransitioning) {
final scrollCompleter = Completer<void>();
_stopScroll(canceled: true);
SchedulerBinding.instance.addPostFrameCallback((_) {
_startScroll(
SchedulerBinding.instance.addPostFrameCallback((_) async {
await _startScroll(
index: index,
alignment: alignment,
duration: duration,
curve: curve,
opacityAnimationWeights: opacityAnimationWeights,
);
scrollCompleter.complete();
});
await scrollCompleter.future;
} else {
await _startScroll(
index: index,
@@ -486,10 +516,11 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
startAnimationCallback = () {
SchedulerBinding.instance.addPostFrameCallback((_) {
startAnimationCallback = () {};
opacity.parent = _opacityAnimation(opacityAnimationWeights).animate(
AnimationController(vsync: this, duration: duration)..forward(),
);
_animationController?.dispose();
_animationController =
AnimationController(vsync: this, duration: duration)..forward();
opacity.parent = _opacityAnimation(opacityAnimationWeights)
.animate(_animationController!);
secondary.scrollController.jumpTo(-direction *
(_screenScrollCount *
primary.scrollController.position.viewportDimension -
@@ -532,17 +563,19 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
}
}
setState(() {
if (opacity.value >= 0.5) {
// Secondary [ListView] is more visible than the primary; make it the
// new primary.
final temp = primary;
primary = secondary;
secondary = temp;
}
_isTransitioning = false;
opacity.parent = const AlwaysStoppedAnimation<double>(0);
});
if (mounted) {
setState(() {
if (opacity.value >= 0.5) {
// Secondary [ListView] is more visible than the primary; make it the
// new primary.
final temp = primary;
primary = secondary;
secondary = temp;
}
_isTransitioning = false;
opacity.parent = const AlwaysStoppedAnimation<double>(0);
});
}
}
Animatable<double> _opacityAnimation(List<double> opacityAnimationWeights) {
@@ -2,8 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: lines_longer_than_80_chars
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
@@ -15,7 +13,7 @@ import 'package:flutter/widgets.dart';
/// Version of [Viewport] with some modifications to how extents are
/// computed to allow scroll extents outside 0 to 1. See [Viewport]
/// for more information.
/// description
/// {@endtemplate}
class UnboundedViewport extends Viewport {
/// {@macro unbounded_viewport}
UnboundedViewport({
@@ -37,15 +35,16 @@ class UnboundedViewport extends Viewport {
double get anchor => _anchor;
@override
RenderViewport createRenderObject(BuildContext context) =>
UnboundedRenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ??
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
);
RenderViewport createRenderObject(BuildContext context) {
return UnboundedRenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ??
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
);
}
}
/// A render object that is bigger on the inside.
@@ -137,14 +136,20 @@ class UnboundedRenderViewport extends RenderViewport {
@override
void performLayout() {
if (center == null) {
assert(firstChild == null, 'firstChild cannot be null');
assert(
firstChild == null,
'A RenderViewport with no center render object must have no children.',
);
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
offset.applyContentDimensions(0, 0);
return;
}
assert(center!.parent == this, 'center.parent cannot be equal to this');
assert(
center!.parent == this,
'''The "center" property of a RenderViewport must be a child of the viewport.''',
);
late double mainAxisExtent;
late double crossAxisExtent;
@@ -186,7 +191,7 @@ class UnboundedRenderViewport extends RenderViewport {
} while (count < _maxLayoutCycles);
assert(() {
if (count >= _maxLayoutCycles) {
assert(count != 1, 'count not equal to 1');
assert(count != 1);
throw FlutterError(
'A RenderViewport exceeded its maximum number of layout cycles.\n'
'RenderViewport render objects, during layout, can retry if either their '
@@ -207,7 +212,7 @@ class UnboundedRenderViewport extends RenderViewport {
);
}
return true;
}(), 'count needs to be bigger than _maxLayoutCycles');
}());
}
double _attemptLayout(
@@ -215,11 +220,11 @@ class UnboundedRenderViewport extends RenderViewport {
double crossAxisExtent,
double correctedOffset,
) {
assert(!mainAxisExtent.isNaN, 'assert mainAxisExtent.isNaN');
assert(mainAxisExtent >= 0.0, 'assert mainAxisExtent >= 0.0');
assert(crossAxisExtent.isFinite, 'assert crossAxisExtent.isFinite');
assert(crossAxisExtent >= 0.0, 'assert crossAxisExtent >= 0.0');
assert(correctedOffset.isFinite, 'assert correctedOffset.isFinite');
assert(!mainAxisExtent.isNaN, 'The main axis extent cannot be NaN.');
assert(mainAxisExtent >= 0.0, 'The main axis extent cannot be negative.');
assert(crossAxisExtent.isFinite, 'The cross axis extent must be finite.');
assert(crossAxisExtent >= 0.0, 'The cross axis extent cannot be negative.');
assert(correctedOffset.isFinite, 'The corrected offset must be finite.');
_minScrollExtent = 0.0;
_maxScrollExtent = 0.0;
_hasVisualOverflow = false;
File diff suppressed because it is too large Load Diff
@@ -71,7 +71,7 @@ enum SpacingType {
/// A [StreamChannel] ancestor widget is required in order to provide the
/// information about the channels.
///
/// Uses a [ListView.custom] to render the list of channels.
/// Uses a [ScrollablePositionedList] to render the list of channels.
///
/// The UI is rendered based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget's appearance.
@@ -88,8 +88,10 @@ class StreamMessageListView extends StatefulWidget {
this.threadBuilder,
this.onThreadTap,
this.dateDividerBuilder,
this.scrollPhysics =
const ClampingScrollPhysics(), // we need to use ClampingScrollPhysics to avoid the list view to animate and break while loading
// we need to use ClampingScrollPhysics to avoid the list view to bounce
// when we are at the either end of the list view and try to use 'animateTo'
// to animate in the same direction.
this.scrollPhysics = const ClampingScrollPhysics(),
this.initialScrollIndex,
this.initialAlignment,
this.scrollController,
@@ -555,6 +557,10 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
if (valueKey != null) {
final index = messagesIndex[valueKey.value];
if (index != null) {
// The calculation is as follows:
// * Add 2 to the index retrieved to account for the footer and the bottom loader.
// * Multiply the result by 2 to account for the separators between each pair of items.
// * Subtract 1 to adjust for the 0-based indexing of the list view.
return ((index + 2) * 2) - 1;
}
}
@@ -2,13 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0;
const screenHeight = 100.0;
const screenWidth = 400.0;
const itemWidth = screenWidth / 10.0;
const itemCount = 500;
@@ -46,6 +45,11 @@ void main() {
);
}
final fadeTransitionFinder = find.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition),
);
testWidgets('List positioned with 0 at left', (WidgetTester tester) async {
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
@@ -172,7 +176,7 @@ void main() {
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('Item 100')).dx, 0);
expect(tester.getBottomRight(find.text('Item 109')).dy, screenWidth);
expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight);
expect(
itemPositionsListener.itemPositions.value
@@ -196,6 +200,31 @@ void main() {
1);
});
testWidgets('Scroll to 20 without fading', (WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
final initialOpacity = fadeTransition.opacity;
unawaited(
itemScrollController.scrollTo(index: 20, duration: scrollDuration));
await tester.pump();
await tester.pump();
await tester.pump(scrollDuration ~/ 2);
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
expect(fadeTransition.opacity, initialOpacity);
await tester.pumpAndSettle();
expect(find.text('Item 14'), findsNothing);
expect(find.text('Item 20'), findsOneWidget);
});
testWidgets('padding test - centered sliver at left',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
@@ -360,4 +360,55 @@ void main() {
.itemTrailingEdge,
1);
});
testWidgets('Does not crash when updated offscreen',
(WidgetTester tester) async {
late StateSetter setState;
var updated = false;
// There's 0 relayout boundaries in this subtree.
final widget = StatefulBuilder(builder: (context, stateSetter) {
setState = stateSetter;
return Positioned(
left: 0,
right: 0,
child: PositionedList(
shrinkWrap: true,
itemCount: 1,
// When `updated` becomes true this line inserts a
// RenderIndexedSemantics to the render tree.
addSemanticIndexes: updated,
itemBuilder: (context, index) => const SizedBox(height: itemHeight),
));
});
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: [
OverlayEntry(builder: (context) => widget, maintainState: true),
],
),
));
// Insert a new opaque OverlayEntry that would prevent the first OverlayEntry
// from doing re-layout. Since there's no relayout boundaries in the first
// OverlayEntry, no dirty RenderObjects in its render subtree can update
// layout.
final newOverlay =
OverlayEntry(builder: (context) => const SizedBox.expand(), opaque: true);
tester.state<OverlayState>(find.byType(Overlay)).insert(newOverlay);
await tester.pump();
// Update the list item's render tree. A new RenderObjectElement is
// inflated, registeredElement.renderObject will point to this new
// RenderObjectElement's RenderObject (RenderIndexedSemantics), which has
// never been laid out.
setState(() {
updated = true;
});
await tester.pump();
expect(tester.takeException(), isNull);
});
}
@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0;
@@ -2,12 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
@@ -48,8 +48,7 @@ void main() {
itemCount: itemCount,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
assert(index >= 0 && index <= itemCount - 1,
'''index needs to be bigger or equal to 0 and smallert than itemCount -1''');
assert(index >= 0 && index <= itemCount - 1);
return SizedBox(
height:
variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight,
@@ -71,6 +70,11 @@ void main() {
);
}
final fadeTransitionFinder = find.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition),
);
testWidgets('List positioned with 0 at top', (WidgetTester tester) async {
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
@@ -394,11 +398,7 @@ void main() {
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
var fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
final initialOpacity = fadeTransition.opacity;
unawaited(
@@ -407,11 +407,7 @@ void main() {
await tester.pump();
await tester.pump(scrollDuration ~/ 2);
fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
expect(fadeTransition.opacity, initialOpacity);
await tester.pumpAndSettle();
@@ -456,10 +452,6 @@ void main() {
final itemScrollController = ItemScrollController();
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
final fadeTransitionFinder = find.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition));
unawaited(
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
await tester.pump();
@@ -533,26 +525,14 @@ void main() {
await tester.pump();
await tester.pump();
expect(
tester
.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last)
.opacity
.value,
closeTo(0, 0.01));
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
closeTo(0, 0.01),
);
await tester.pump(scrollDuration + scrollDurationTolerance);
expect(
tester
.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last)
.opacity
.value,
closeTo(1, 0.01));
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
closeTo(1, 0.01),
);
expect(find.text('Item 0'), findsOneWidget);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
@@ -610,15 +590,9 @@ void main() {
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight);
expect(
tester
.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last)
.opacity
.value,
closeTo(0.5, 0.01));
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
closeTo(0.5, 0.01),
);
await tester.pumpAndSettle();
});
@@ -899,11 +873,7 @@ void main() {
await tester.pump();
expect(tester.getTopLeft(find.text('Item 9')).dy, 0);
final fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
expect(fadeTransition.opacity.value, 1.0);
await tester.pumpAndSettle();
@@ -923,21 +893,12 @@ void main() {
await tester.pump();
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
final fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
expect(fadeTransition.opacity.value, 1.0);
await tester.pumpAndSettle();
});
final fadeTransitionFinder = find.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition),
);
testWidgets('Scroll to 0 stop before half way', (WidgetTester tester) async {
final itemScrollController = ItemScrollController();
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
@@ -1022,14 +983,13 @@ void main() {
itemScrollController.scrollTo(index: 0, duration: scrollDuration));
await tester.pump();
await tester.pump();
await tester.pump(scrollDuration ~/ 2 + scrollDuration ~/ 20);
await tester.pump(scrollDuration ~/ 2);
await tester.tap(find.byType(ScrollablePositionedList));
await tester.pump();
expect(tester.getTopLeft(find.text('Item 9')).dy, closeTo(0, tolerance));
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
expect(fadeTransition.opacity.value, 1.0);
expect(tester.getTopLeft(find.text('Item 90')).dy, 0);
expect(fadeTransitionFinder, findsNWidgets(1));
await tester.pumpAndSettle();
});
@@ -1098,6 +1058,34 @@ void main() {
expect(find.text('Item 100'), findsNothing);
});
testWidgets("Second scroll future doesn't complete until scroll is done",
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
unawaited(
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
await tester.pump();
await tester.pump();
await tester.pump(scrollDuration ~/ 2);
final scrollFuture2 =
itemScrollController.scrollTo(index: 250, duration: scrollDuration);
var futureComplete = false;
unawaited(scrollFuture2.then((_) => futureComplete = true));
await tester.pump();
await tester.pump();
await tester.pump(scrollDuration ~/ 2);
expect(futureComplete, isFalse);
await tester.pumpAndSettle();
expect(futureComplete, isTrue);
});
testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
@@ -1145,7 +1133,7 @@ void main() {
}, skip: true);
testWidgets(
'''Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back''',
'Jump to 400 at bottom, manually scroll, scroll to 100 at bottom and back',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
@@ -1664,7 +1652,7 @@ void main() {
});
testWidgets(
'''Maintain programmatic and user position (9 half way off top) in page view''',
'Maintain programmatic and user position (9 half way off top) in page view',
(WidgetTester tester) async {
final itemPositionsListener = ItemPositionsListener.create();
final itemScrollController = ItemScrollController();
@@ -1751,21 +1739,21 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<int>(
valueListenable: itemCount,
builder: (context, itemCount, child) =>
ScrollablePositionedList.builder(
initialScrollIndex: min(100, itemCount),
itemCount: itemCount,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) {
assert(index >= 0 && index <= itemCount - 1,
'index not bigger than 0 and smaller than itemCount - 1');
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
),
builder: (context, itemCount, child) {
return ScrollablePositionedList.builder(
initialScrollIndex: min(100, itemCount),
itemCount: itemCount,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) {
assert(index >= 0 && index <= itemCount - 1);
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
);
},
),
),
);
@@ -1795,19 +1783,19 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<int>(
valueListenable: itemCount,
builder: (context, itemCount, child) =>
ScrollablePositionedList.builder(
initialScrollIndex: min(100, itemCount - 1),
itemCount: itemCount,
itemBuilder: (context, index) {
assert(index >= 0 && index <= itemCount - 1,
'index not bigger than 0 and smaller than itemCount -1');
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
),
builder: (context, itemCount, child) {
return ScrollablePositionedList.builder(
initialScrollIndex: min(100, itemCount - 1),
itemCount: itemCount,
itemBuilder: (context, index) {
assert(index >= 0 && index <= itemCount - 1);
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
);
},
),
),
);
@@ -1834,19 +1822,19 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<int>(
valueListenable: itemCount,
builder: (context, itemCount, child) =>
ScrollablePositionedList.builder(
initialScrollIndex: itemCount - 1,
itemCount: itemCount,
itemBuilder: (context, index) {
assert(index >= 0 && index <= itemCount - 1,
'index not bigger than 0 and smaller than itemCount -1');
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
),
builder: (context, itemCount, child) {
return ScrollablePositionedList.builder(
initialScrollIndex: itemCount - 1,
itemCount: itemCount,
itemBuilder: (context, index) {
assert(index >= 0 && index <= itemCount - 1);
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
);
},
),
),
);
@@ -1878,11 +1866,7 @@ void main() {
minCacheExtent: 10,
);
var fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
final initialOpacity = fadeTransition.opacity;
unawaited(
@@ -1891,11 +1875,7 @@ void main() {
await tester.pump();
await tester.pump(scrollDuration ~/ 2);
fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
expect(fadeTransition.opacity, initialOpacity);
await tester.pumpAndSettle();
@@ -1914,11 +1894,9 @@ void main() {
minCacheExtent: itemHeight * 200,
);
var fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
var fadeTransition = tester.widget<FadeTransition>(
fadeTransitionFinder,
);
final initialOpacity = fadeTransition.opacity;
unawaited(
@@ -1927,11 +1905,7 @@ void main() {
await tester.pump();
await tester.pump(scrollDuration ~/ 2);
fadeTransition = tester.widget<FadeTransition>(find
.descendant(
of: find.byType(ScrollablePositionedList),
matching: find.byType(FadeTransition))
.last);
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
expect(fadeTransition.opacity, initialOpacity);
await tester.pumpAndSettle();
@@ -1965,17 +1939,21 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<Key>(
valueListenable: key,
builder: (context, key, child) => Container(
key: key,
child: ScrollablePositionedList.builder(
itemCount: 200,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
builder: (context, key, child) {
return Container(
key: key,
child: ScrollablePositionedList.builder(
itemCount: 200,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
),
),
),
);
},
),
),
);
@@ -2054,15 +2032,19 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<Key>(
valueListenable: key,
builder: (context, key, child) => ScrollablePositionedList.builder(
key: key,
itemCount: 10,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
),
builder: (context, key, child) {
return ScrollablePositionedList.builder(
key: key,
itemCount: 10,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
);
},
),
),
);
@@ -2084,17 +2066,21 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<Key>(
valueListenable: key,
builder: (context, key, child) => Container(
key: key,
child: ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
builder: (context, key, child) {
return Container(
key: key,
child: ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
),
),
),
);
},
),
),
);
@@ -2124,18 +2110,22 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<Key>(
valueListenable: containerKey,
builder: (context, key, child) => Container(
key: key,
child: ScrollablePositionedList.builder(
key: scrollKey,
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
builder: (context, key, child) {
return Container(
key: key,
child: ScrollablePositionedList.builder(
key: scrollKey,
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
),
),
),
);
},
),
),
);
@@ -2166,15 +2156,18 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<ItemScrollController>(
valueListenable: itemScrollControllerListenable,
builder: (context, itemScrollController, child) =>
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
),
builder: (context, itemScrollController, child) {
return ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
);
},
),
),
);
@@ -2215,29 +2208,35 @@ void main() {
Expanded(
child: ValueListenableBuilder<ItemScrollController>(
valueListenable: topItemScrollControllerListenable,
builder: (context, itemScrollController, child) =>
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
),
builder: (context, itemScrollController, child) {
return ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
);
},
),
),
Expanded(
child: ValueListenableBuilder<ItemScrollController>(
valueListenable: bottomItemScrollControllerListenable,
builder: (context, itemScrollController, child) =>
ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
),
builder: (context, itemScrollController, child) {
return ScrollablePositionedList.builder(
itemCount: 100,
itemScrollController: itemScrollController,
itemBuilder: (context, index) {
return SizedBox(
height: itemHeight,
child: Text('Item $index'),
);
},
);
},
),
),
],
@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
@@ -497,20 +496,21 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<int>(
valueListenable: itemCount,
builder: (context, itemCount, child) =>
ScrollablePositionedList.separated(
itemCount: itemCount,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
separatorBuilder: (context, index) => SizedBox(
height: separatorHeight,
child: Text('Separator $index'),
),
),
builder: (context, itemCount, child) {
return ScrollablePositionedList.separated(
itemCount: itemCount,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
separatorBuilder: (context, index) => SizedBox(
height: separatorHeight,
child: Text('Separator $index'),
),
);
},
),
),
);
@@ -538,20 +538,21 @@ void main() {
MaterialApp(
home: ValueListenableBuilder<int>(
valueListenable: itemCount,
builder: (context, itemCount, child) =>
ScrollablePositionedList.separated(
itemCount: itemCount,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
separatorBuilder: (context, index) => SizedBox(
height: separatorHeight,
child: Text('Separator $index'),
),
),
builder: (context, itemCount, child) {
return ScrollablePositionedList.separated(
itemCount: itemCount,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
separatorBuilder: (context, index) => SizedBox(
height: separatorHeight,
child: Text('Separator $index'),
),
);
},
),
),
);
@@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0;
@@ -0,0 +1,473 @@
// Copyright 2019 The Fuchsia 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/src/positioned_list.dart';
const screenHeight = 400.0;
const screenWidth = 400.0;
const itemHeight = screenHeight / 10.0;
const defaultItemCount = 500;
void main() {
final itemPositionsNotifier = ItemPositionsListener.create();
Future<void> setUpWidgetTest(
WidgetTester tester, {
int topItem = 0,
Key? key,
ScrollController? scrollController,
double anchor = 0,
int itemCount = defaultItemCount,
bool reverse = false,
}) async {
tester.binding.window.devicePixelRatioTestValue = 1.0;
tester.binding.window.physicalSizeTestValue =
const Size(screenWidth, screenHeight);
await tester.pumpWidget(
MaterialApp(
// Use flex layout to ensure that the minimum height is not limited to screenHeight
home: Column(children: [
// Use Constrained to make max height not more than screenHeight
ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth),
child: PositionedList(
key: key,
itemCount: itemCount,
positionedIndex: topItem,
alignment: anchor,
controller: scrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
itemPositionsNotifier:
itemPositionsNotifier as ItemPositionsNotifier,
shrinkWrap: true,
reverse: reverse,
),
),
]),
),
);
}
testWidgets('short list with shrink wrap', (WidgetTester tester) async {
const itemCount = 5;
const key = Key('short_list');
await setUpWidgetTest(tester, itemCount: itemCount, key: key);
await tester.pump();
expect(
tester.getBottomRight(find.text('Item 4')).dy, itemHeight * itemCount);
expect(find.text('Item 4'), findsOneWidget);
expect(find.text('Item 5'), findsNothing);
final positionList = find.byKey(key);
expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 4)
.itemTrailingEdge,
1.0);
});
testWidgets('List positioned with 0 at top and shrink wrap',
(WidgetTester tester) async {
await setUpWidgetTest(tester);
await tester.pump();
expect(find.text('Item 0'), findsOneWidget);
expect(find.text('Item 9'), findsOneWidget);
expect(find.text('Item 10'), findsNothing);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 9)
.itemTrailingEdge,
1);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 10)
.itemLeadingEdge,
1);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 10)
.itemTrailingEdge,
11 / 10);
});
testWidgets('List positioned with 5 at top and shrink wrap',
(WidgetTester tester) async {
await setUpWidgetTest(tester, topItem: 5);
await tester.pump();
expect(find.text('Item 4'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
expect(find.text('Item 14'), findsOneWidget);
expect(find.text('Item 15'), findsNothing);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 4)
.itemLeadingEdge,
-1 / 10);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 4)
.itemTrailingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 5)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 14)
.itemTrailingEdge,
1);
});
testWidgets('List positioned with 20 at bottom and shrink wrap',
(WidgetTester tester) async {
await setUpWidgetTest(tester, topItem: 20, anchor: 1);
await tester.pump();
expect(find.text('Item 20'), findsNothing);
expect(find.text('Item 19'), findsOneWidget);
expect(find.text('Item 10'), findsOneWidget);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 10)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 19)
.itemLeadingEdge,
9 / 10);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 19)
.itemTrailingEdge,
1);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 20)
.itemLeadingEdge,
1);
});
testWidgets('List positioned with 20 at halfway and shrink wrap',
(WidgetTester tester) async {
await setUpWidgetTest(tester, topItem: 20, anchor: 0.5);
await tester.pump();
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 20)
.itemLeadingEdge,
0.5);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 20)
.itemTrailingEdge,
0.5 + itemHeight / screenHeight);
});
testWidgets('List positioned with 20 half off top of screen and shrink wrap',
(WidgetTester tester) async {
await setUpWidgetTest(tester,
topItem: 20, anchor: -(itemHeight / screenHeight) / 2);
await tester.pump();
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 20)
.itemLeadingEdge,
-(itemHeight / screenHeight) / 2);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 20)
.itemTrailingEdge,
(itemHeight / screenHeight) / 2);
});
testWidgets('List positioned with 5 at top then scroll up 2 and shrink wrap',
(WidgetTester tester) async {
await setUpWidgetTest(tester, topItem: 5);
await tester.drag(
find.byType(PositionedList), const Offset(0, itemHeight * 2));
await tester.pump();
expect(find.text('Item 2'), findsNothing);
expect(find.text('Item 3'), findsOneWidget);
expect(find.text('Item 12'), findsOneWidget);
expect(find.text('Item 13'), findsNothing);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 2)
.itemLeadingEdge,
-1 / 10);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 3)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 12)
.itemTrailingEdge,
1);
});
testWidgets(
'List positioned with 5 at top then scroll down 1/2 and shrink wrap',
(WidgetTester tester) async {
await setUpWidgetTest(tester, topItem: 5);
await tester.drag(
find.byType(PositionedList), const Offset(0, -1 / 2 * itemHeight));
await tester.pump();
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 5)
.itemTrailingEdge,
1 / 20);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 14)
.itemLeadingEdge,
17 / 20);
});
testWidgets('List positioned with 0 at top scroll up 5 and shrink wrap',
(WidgetTester tester) async {
final scrollController = ScrollController();
await setUpWidgetTest(tester, scrollController: scrollController);
await tester.pump();
scrollController.jumpTo(itemHeight * 5);
await tester.pump();
await tester.pumpAndSettle();
expect(find.text('Item 4'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
expect(find.text('Item 14'), findsOneWidget);
expect(find.text('Item 15'), findsNothing);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 5)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 4)
.itemLeadingEdge,
-1 / 10);
});
testWidgets(
'List positioned with 5 at top then scroll up 2 programatically and shrink wrap',
(WidgetTester tester) async {
final scrollController = ScrollController();
await setUpWidgetTest(tester,
topItem: 5, scrollController: scrollController);
scrollController.jumpTo(-2 * itemHeight);
await tester.pump();
expect(find.text('Item 2'), findsNothing);
expect(find.text('Item 3'), findsOneWidget);
expect(find.text('Item 12'), findsOneWidget);
expect(find.text('Item 13'), findsNothing);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 2)
.itemLeadingEdge,
-1 / 10);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 3)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 12)
.itemTrailingEdge,
1);
});
testWidgets(
'List positioned with 5 at top then scroll down 20 programatically and shrink wrap',
(WidgetTester tester) async {
final scrollController = ScrollController();
await setUpWidgetTest(tester,
topItem: 5, scrollController: scrollController);
scrollController.jumpTo(itemHeight * 20);
await tester.pump();
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 23)
.itemLeadingEdge,
-2 / 10);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 24)
.itemLeadingEdge,
-1 / 10);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 25)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 4)
.itemLeadingEdge,
-21 / 10);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 5)
.itemLeadingEdge,
-20 / 10);
});
testWidgets(
'List positioned with 5 at top and initial scroll offset and shrink wrap',
(WidgetTester tester) async {
final scrollController =
ScrollController(initialScrollOffset: -2 * itemHeight);
await setUpWidgetTest(tester,
topItem: 5, scrollController: scrollController);
expect(find.text('Item 2'), findsNothing);
expect(find.text('Item 3'), findsOneWidget);
expect(find.text('Item 12'), findsOneWidget);
expect(find.text('Item 13'), findsNothing);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 3)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 12)
.itemTrailingEdge,
1);
});
testWidgets('short List with reverse and shrink wrap',
(WidgetTester tester) async {
const itemCount = 5;
const key = Key('short_list');
await setUpWidgetTest(tester,
itemCount: itemCount, key: key, reverse: true);
await tester.pump();
expect(find.text('Item 4'), findsOneWidget);
expect(find.text('Item 5'), findsNothing);
expect(
tester.getBottomRight(find.text('Item 0')).dy, itemHeight * itemCount);
expect(tester.getTopLeft(find.text('Item 4')).dy, 0);
final positionList = find.byKey(key);
expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount);
expect(tester.getTopLeft(positionList).dy, 0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 4)
.itemTrailingEdge,
1.0);
});
testWidgets('test nested positioned list', (WidgetTester tester) async {
const itemCount = 50;
const key = Key('short_list');
tester.binding.window.devicePixelRatioTestValue = 1.0;
tester.binding.window.physicalSizeTestValue =
const Size(screenWidth, screenHeight);
await tester.pumpWidget(
MaterialApp(
// Use flex layout to ensure that the minimum height is not limited to screenHeight
home: PositionedList(
itemCount: 5,
itemBuilder: (context, index) {
if (index == 0) {
return PositionedList(
key: key,
itemCount: itemCount,
shrinkWrap: true,
itemBuilder: (context, idx) => SizedBox(
height: itemHeight,
child: Text('Item $idx'),
));
} else {
return SizedBox(
height: itemHeight,
child: Text('Item ${itemCount + index - 1}'),
);
}
},
itemPositionsNotifier: itemPositionsNotifier as ItemPositionsNotifier,
),
),
);
await tester.pump();
expect(find.text('Item 0'), findsOneWidget);
expect(find.text('Item 50'), findsNothing);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
expect(tester.getBottomRight(find.text('Item 9')).dy, screenHeight);
final positionList = find.byKey(key);
expect(tester.getBottomRight(positionList).dy, itemHeight * itemCount);
expect(tester.getTopLeft(positionList).dy, 0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemLeadingEdge,
0);
expect(
itemPositionsNotifier.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemTrailingEdge,
5.0);
});
}
@@ -0,0 +1,246 @@
// Copyright 2019 The Fuchsia 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pedantic/pedantic.dart';
import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positioned_list.dart';
const screenHeight = 400.0;
const screenWidth = 400.0;
const itemHeight = screenHeight / 10.0;
const itemCount = 500;
const scrollDuration = Duration(seconds: 1);
void main() {
Future<void> setUpWidgetTest(
WidgetTester tester, {
ItemScrollController? itemScrollController,
ItemPositionsListener? itemPositionsListener,
EdgeInsets? padding,
int initialIndex = 0,
}) async {
tester.binding.window.devicePixelRatioTestValue = 1.0;
tester.binding.window.physicalSizeTestValue =
const Size(screenWidth, screenHeight);
await tester.pumpWidget(
MaterialApp(
// Use flex layout to ensure that the minimum height is not limited to screenHeight
home: Column(children: [
// Use Constrained to make max height not more than screenHeight
ConstrainedBox(
constraints:
const BoxConstraints(maxHeight: screenHeight, maxWidth: screenWidth),
child: ScrollablePositionedList.builder(
itemCount: itemCount,
initialScrollIndex: initialIndex,
itemScrollController: itemScrollController,
itemBuilder: (context, index) => SizedBox(
height: itemHeight,
child: Text('Item $index'),
),
itemPositionsListener: itemPositionsListener,
shrinkWrap: true,
padding: padding,
),
),
]),
),
);
}
testWidgets('List positioned with 0 at top and shrink wrap',
(WidgetTester tester) async {
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
expect(tester.getBottomRight(find.text('Item 9')).dy, screenHeight);
expect(find.text('Item 10'), findsNothing);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 9)
.itemTrailingEdge,
1);
});
testWidgets('Scroll to 1 then 2 (both already on screen) with shrink wrap',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
unawaited(
itemScrollController.scrollTo(index: 1, duration: scrollDuration));
await tester.pump();
await tester.pump(scrollDuration);
expect(find.text('Item 0'), findsNothing);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 1)
.itemLeadingEdge,
0);
expect(tester.getTopLeft(find.text('Item 1')).dy, 0);
unawaited(
itemScrollController.scrollTo(index: 2, duration: scrollDuration));
await tester.pump();
await tester.pump(scrollDuration);
expect(find.text('Item 1'), findsNothing);
expect(tester.getTopLeft(find.text('Item 2')).dy, 0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 2)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 11)
.itemTrailingEdge,
1);
});
testWidgets(
'Scroll to 5 (already on screen) and then back to 0 with shrink wrap',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
unawaited(
itemScrollController.scrollTo(index: 5, duration: scrollDuration));
await tester.pumpAndSettle();
unawaited(
itemScrollController.scrollTo(index: 0, duration: scrollDuration));
await tester.pumpAndSettle();
expect(find.text('Item 0'), findsOneWidget);
expect(find.text('Item 9'), findsOneWidget);
expect(find.text('Item 10'), findsNothing);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 0)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 9)
.itemTrailingEdge,
1);
});
testWidgets('Scroll to 100 (not already on screen) with shrink wrap',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
unawaited(
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
await tester.pumpAndSettle();
expect(find.text('Item 99'), findsNothing);
expect(find.text('Item 100'), findsOneWidget);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 100)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 109)
.itemTrailingEdge,
1);
});
testWidgets('Jump to 100 with shrink wrap', (WidgetTester tester) async {
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();
await setUpWidgetTest(tester,
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener);
itemScrollController.jumpTo(index: 100);
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('Item 100')).dy, 0);
expect(tester.getBottomRight(find.text('Item 109')).dy, screenHeight);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 100)
.itemLeadingEdge,
0);
expect(
itemPositionsListener.itemPositions.value
.firstWhere((position) => position.index == 109)
.itemTrailingEdge,
1);
});
testWidgets('padding test - centered sliver at bottom with shrink wrap',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
await setUpWidgetTest(
tester,
itemScrollController: itemScrollController,
padding: const EdgeInsets.all(10),
);
expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10));
expect(tester.getTopLeft(find.text('Item 1')),
const Offset(10, itemHeight + 10));
expect(tester.getBottomRight(find.text('Item 1')),
const Offset(screenWidth - 10, 10 + itemHeight * 2));
unawaited(
itemScrollController.scrollTo(index: 490, duration: scrollDuration));
await tester.pumpAndSettle();
await tester.drag(
find.byType(ScrollablePositionedList), const Offset(0, -100));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('Item 499')),
const Offset(10, screenHeight - itemHeight - 10));
});
testWidgets('padding test - centered sliver not at bottom',
(WidgetTester tester) async {
final itemScrollController = ItemScrollController();
await setUpWidgetTest(
tester,
itemScrollController: itemScrollController,
initialIndex: 2,
padding: const EdgeInsets.all(10),
);
await tester.drag(
find.byType(ScrollablePositionedList), const Offset(0, 200));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('Item 0')), const Offset(10, 10));
expect(tester.getTopLeft(find.text('Item 2')),
const Offset(10, 10 + itemHeight * 2));
expect(tester.getTopLeft(find.text('Item 3')),
const Offset(10, 10 + itemHeight * 3));
});
}