Merge pull request #1519 from GetStream/feat/update-spl
This commit is contained in:
@@ -52,6 +52,8 @@
|
|||||||
),
|
),
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Added `StreamMessageListView.shrinkWrap` to allow users to shrink wrap the message list view.
|
||||||
|
|
||||||
🔄 Changed
|
🔄 Changed
|
||||||
|
|
||||||
- Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`.
|
- Deprecated `MessageTheme.linkBackgroundColor` in favor of `MessageTheme.urlAttachmentBackgroundColor`.
|
||||||
|
|||||||
+59
-34
@@ -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_listener.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/item_positions_notifier.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/scroll_view.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart';
|
||||||
|
|
||||||
/// A list of widgets similar to [ListView], except scroll control
|
/// A list of widgets similar to [ListView], except scroll control
|
||||||
/// and position reporting is based on index rather than pixel offset.
|
/// and position reporting is based on index rather than pixel offset.
|
||||||
@@ -35,28 +36,20 @@ class PositionedList extends StatefulWidget {
|
|||||||
this.alignment = 0,
|
this.alignment = 0,
|
||||||
this.scrollDirection = Axis.vertical,
|
this.scrollDirection = Axis.vertical,
|
||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
|
this.shrinkWrap = false,
|
||||||
this.physics,
|
this.physics,
|
||||||
this.padding,
|
this.padding,
|
||||||
this.cacheExtent,
|
this.cacheExtent,
|
||||||
this.semanticChildCount,
|
this.semanticChildCount,
|
||||||
this.findChildIndexCallback,
|
|
||||||
this.addSemanticIndexes = true,
|
this.addSemanticIndexes = true,
|
||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.addAutomaticKeepAlives = true,
|
this.addAutomaticKeepAlives = true,
|
||||||
this.keyboardDismissBehavior,
|
this.findChildIndexCallback,
|
||||||
}) : assert((positionedIndex == 0) || (positionedIndex < itemCount),
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
'positionedIndex cannot be 0 and must be smaller than itemCount');
|
}) : assert(
|
||||||
|
(positionedIndex == 0) || (positionedIndex < itemCount),
|
||||||
/// Called to find the new index of a child based on its key in case of
|
'positionedIndex must be 0 or a value less than itemCount',
|
||||||
/// 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;
|
|
||||||
|
|
||||||
/// Number of items the [itemBuilder] can produce.
|
/// Number of items the [itemBuilder] can produce.
|
||||||
final int itemCount;
|
final int itemCount;
|
||||||
@@ -98,6 +91,15 @@ class PositionedList extends StatefulWidget {
|
|||||||
/// See [ScrollView.reverse].
|
/// See [ScrollView.reverse].
|
||||||
final bool 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.
|
/// How the scroll view should respond to user input.
|
||||||
///
|
///
|
||||||
/// For example, determines how the scroll view continues to animate after the
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
@@ -132,9 +134,22 @@ class PositionedList extends StatefulWidget {
|
|||||||
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
||||||
final bool addAutomaticKeepAlives;
|
final bool addAutomaticKeepAlives;
|
||||||
|
|
||||||
/// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will
|
/// Called to find the new index of a child based on its key in case of reordering.
|
||||||
/// dismiss the keyboard automatically.
|
///
|
||||||
final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior;
|
/// 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
|
@override
|
||||||
State<StatefulWidget> createState() => _PositionedListState();
|
State<StatefulWidget> createState() => _PositionedListState();
|
||||||
@@ -175,12 +190,13 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
anchor: widget.alignment,
|
anchor: widget.alignment,
|
||||||
center: _centerKey,
|
center: _centerKey,
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
cacheExtent: widget.cacheExtent,
|
cacheExtent: widget.cacheExtent,
|
||||||
physics: widget.physics,
|
physics: widget.physics,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
semanticChildCount: widget.semanticChildCount ?? widget.itemCount,
|
semanticChildCount: widget.semanticChildCount ?? widget.itemCount,
|
||||||
|
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
||||||
slivers: <Widget>[
|
slivers: <Widget>[
|
||||||
if (widget.positionedIndex > 0)
|
if (widget.positionedIndex > 0)
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
@@ -196,9 +212,9 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
? widget.positionedIndex
|
? widget.positionedIndex
|
||||||
: widget.positionedIndex * 2,
|
: widget.positionedIndex * 2,
|
||||||
addSemanticIndexes: false,
|
addSemanticIndexes: false,
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -213,10 +229,10 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
index + widget.positionedIndex * 2,
|
index + widget.positionedIndex * 2,
|
||||||
),
|
),
|
||||||
childCount: widget.itemCount != 0 ? 1 : 0,
|
childCount: widget.itemCount != 0 ? 1 : 0,
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
|
||||||
addSemanticIndexes: false,
|
addSemanticIndexes: false,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -234,10 +250,10 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
childCount: widget.separatorBuilder == null
|
childCount: widget.separatorBuilder == null
|
||||||
? widget.itemCount - widget.positionedIndex - 1
|
? widget.itemCount - widget.positionedIndex - 1
|
||||||
: 2 * (widget.itemCount - widget.positionedIndex - 1),
|
: 2 * (widget.itemCount - widget.positionedIndex - 1),
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
|
||||||
addSemanticIndexes: false,
|
addSemanticIndexes: false,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
|
findChildIndexCallback: widget.findChildIndexCallback,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -319,25 +335,33 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
if (!updateScheduled) {
|
if (!updateScheduled) {
|
||||||
updateScheduled = true;
|
updateScheduled = true;
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
if (registeredElements.value == null) {
|
final elements = registeredElements.value;
|
||||||
|
if (elements == null) {
|
||||||
updateScheduled = false;
|
updateScheduled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final positions = <ItemPosition>[];
|
final positions = <ItemPosition>[];
|
||||||
RenderViewport? viewport;
|
RenderViewportBase? viewport;
|
||||||
for (final element in registeredElements.value!) {
|
for (final element in elements) {
|
||||||
final box = element.renderObject as RenderBox?;
|
final box = element.renderObject! as RenderBox;
|
||||||
viewport ??= RenderAbstractViewport.of(box) as RenderViewport?;
|
viewport ??= RenderAbstractViewport.of(box) as RenderViewportBase?;
|
||||||
if (viewport == null || box == null) {
|
var anchor = 0.0;
|
||||||
break;
|
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) {
|
if (widget.scrollDirection == Axis.vertical) {
|
||||||
final reveal = viewport.getOffsetToReveal(box, 0).offset;
|
final reveal = viewport!.getOffsetToReveal(box, 0).offset;
|
||||||
if (!reveal.isFinite) continue;
|
if (!reveal.isFinite) continue;
|
||||||
final itemOffset = reveal -
|
final itemOffset =
|
||||||
viewport.offset.pixels +
|
reveal - viewport.offset.pixels + anchor * viewport.size.height;
|
||||||
viewport.anchor * viewport.size.height;
|
|
||||||
positions.add(ItemPosition(
|
positions.add(ItemPosition(
|
||||||
index: key.index,
|
index: key.index,
|
||||||
itemLeadingEdge: itemOffset.round() /
|
itemLeadingEdge: itemOffset.round() /
|
||||||
@@ -348,6 +372,7 @@ class _PositionedListState extends State<PositionedList> {
|
|||||||
} else {
|
} else {
|
||||||
final itemOffset =
|
final itemOffset =
|
||||||
box.localToGlobal(Offset.zero, ancestor: viewport).dx;
|
box.localToGlobal(Offset.zero, ancestor: viewport).dx;
|
||||||
|
if (!itemOffset.isFinite) continue;
|
||||||
positions.add(ItemPosition(
|
positions.add(ItemPosition(
|
||||||
index: key.index,
|
index: key.index,
|
||||||
itemLeadingEdge: (widget.reverse
|
itemLeadingEdge: (widget.reverse
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.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/viewport.dart';
|
||||||
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/wrapping.dart';
|
||||||
|
|
||||||
/// {@template custom_scroll_view}
|
/// {@template unbounded_custom_scroll_view}
|
||||||
/// A version of [CustomScrollView] that does not constrict the extents
|
/// A version of [CustomScrollView] that allows does not constrict the extents
|
||||||
/// to be within 0 and 1. See [CustomScrollView] for more information.
|
/// to be within 0 and 1. See [CustomScrollView] for more information.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
class UnboundedCustomScrollView extends CustomScrollView {
|
class UnboundedCustomScrollView extends CustomScrollView {
|
||||||
/// {@macro custom_scroll_view}
|
/// {@macro unbounded_custom_scroll_view}
|
||||||
const UnboundedCustomScrollView({
|
const UnboundedCustomScrollView({
|
||||||
super.key,
|
super.key,
|
||||||
super.scrollDirection,
|
super.scrollDirection,
|
||||||
@@ -19,19 +20,19 @@ class UnboundedCustomScrollView extends CustomScrollView {
|
|||||||
super.controller,
|
super.controller,
|
||||||
super.primary,
|
super.primary,
|
||||||
super.physics,
|
super.physics,
|
||||||
super.shrinkWrap,
|
bool shrinkWrap = false,
|
||||||
super.center,
|
super.center,
|
||||||
double anchor = 0.0,
|
double anchor = 0.0,
|
||||||
super.cacheExtent,
|
super.cacheExtent,
|
||||||
super.slivers,
|
super.slivers,
|
||||||
super.semanticChildCount,
|
super.semanticChildCount,
|
||||||
super.dragStartBehavior,
|
super.dragStartBehavior,
|
||||||
ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior,
|
super.keyboardDismissBehavior,
|
||||||
}) : _anchor = anchor,
|
}) : _shrinkWrap = shrinkWrap,
|
||||||
super(
|
_anchor = anchor,
|
||||||
keyboardDismissBehavior: keyboardDismissBehavior ??
|
super(shrinkWrap: false);
|
||||||
ScrollViewKeyboardDismissBehavior.manual,
|
|
||||||
);
|
final bool _shrinkWrap;
|
||||||
|
|
||||||
// [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so
|
// [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so
|
||||||
// we need our own version.
|
// we need our own version.
|
||||||
@@ -49,11 +50,14 @@ class UnboundedCustomScrollView extends CustomScrollView {
|
|||||||
AxisDirection axisDirection,
|
AxisDirection axisDirection,
|
||||||
List<Widget> slivers,
|
List<Widget> slivers,
|
||||||
) {
|
) {
|
||||||
if (shrinkWrap) {
|
if (_shrinkWrap) {
|
||||||
return ShrinkWrappingViewport(
|
return CustomShrinkWrappingViewport(
|
||||||
axisDirection: axisDirection,
|
axisDirection: axisDirection,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
slivers: slivers,
|
slivers: slivers,
|
||||||
|
cacheExtent: cacheExtent,
|
||||||
|
center: center,
|
||||||
|
anchor: anchor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return UnboundedViewport(
|
return UnboundedViewport(
|
||||||
|
|||||||
+129
-96
@@ -37,6 +37,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
super.key,
|
super.key,
|
||||||
this.itemScrollController,
|
this.itemScrollController,
|
||||||
|
this.shrinkWrap = false,
|
||||||
ItemPositionsListener? itemPositionsListener,
|
ItemPositionsListener? itemPositionsListener,
|
||||||
this.initialScrollIndex = 0,
|
this.initialScrollIndex = 0,
|
||||||
this.initialAlignment = 0,
|
this.initialAlignment = 0,
|
||||||
@@ -50,7 +51,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
this.keyboardDismissBehavior,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||||
separatorBuilder = null;
|
separatorBuilder = null;
|
||||||
|
|
||||||
@@ -61,6 +62,7 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
required this.itemBuilder,
|
required this.itemBuilder,
|
||||||
required IndexedWidgetBuilder this.separatorBuilder,
|
required IndexedWidgetBuilder this.separatorBuilder,
|
||||||
super.key,
|
super.key,
|
||||||
|
this.shrinkWrap = false,
|
||||||
this.itemScrollController,
|
this.itemScrollController,
|
||||||
ItemPositionsListener? itemPositionsListener,
|
ItemPositionsListener? itemPositionsListener,
|
||||||
this.initialScrollIndex = 0,
|
this.initialScrollIndex = 0,
|
||||||
@@ -75,24 +77,9 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
this.addRepaintBoundaries = true,
|
this.addRepaintBoundaries = true,
|
||||||
this.minCacheExtent,
|
this.minCacheExtent,
|
||||||
this.findChildIndexCallback,
|
this.findChildIndexCallback,
|
||||||
this.keyboardDismissBehavior,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
|
||||||
}) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?;
|
}) : 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.
|
/// Number of items the [itemBuilder] can produce.
|
||||||
final int itemCount;
|
final int itemCount;
|
||||||
|
|
||||||
@@ -131,6 +118,15 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
/// See [ScrollView.reverse].
|
/// See [ScrollView.reverse].
|
||||||
final bool 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.
|
/// How the scroll view should respond to user input.
|
||||||
///
|
///
|
||||||
/// For example, determines how the scroll view continues to animate after the
|
/// For example, determines how the scroll view continues to animate after the
|
||||||
@@ -171,6 +167,23 @@ class ScrollablePositionedList extends StatefulWidget {
|
|||||||
/// cache extent.
|
/// cache extent.
|
||||||
final double? minCacheExtent;
|
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
|
@override
|
||||||
State<StatefulWidget> createState() => _ScrollablePositionedListState();
|
State<StatefulWidget> createState() => _ScrollablePositionedListState();
|
||||||
}
|
}
|
||||||
@@ -233,11 +246,15 @@ class ItemScrollController {
|
|||||||
Curve curve = Curves.linear,
|
Curve curve = Curves.linear,
|
||||||
List<double> opacityAnimationWeights = const [40, 20, 40],
|
List<double> opacityAnimationWeights = const [40, 20, 40],
|
||||||
}) {
|
}) {
|
||||||
assert(_scrollableListState != null, '_scrollableListState cannot be null');
|
assert(
|
||||||
assert(opacityAnimationWeights.length == 3,
|
_scrollableListState != null,
|
||||||
'opacityAnimationWeights.length is not equal to 3');
|
'''ScrollController must be attached to a ScrollablePositionedList to scroll.''',
|
||||||
assert(duration > Duration.zero,
|
);
|
||||||
'duration needs to be bigger than Duration.zero');
|
assert(
|
||||||
|
opacityAnimationWeights.length == 3,
|
||||||
|
'opacityAnimationWeights must have exactly three elements.',
|
||||||
|
);
|
||||||
|
assert(duration > Duration.zero, 'Duration must be greater than zero.');
|
||||||
return _scrollableListState!._scrollTo(
|
return _scrollableListState!._scrollTo(
|
||||||
index: index,
|
index: index,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
@@ -249,7 +266,9 @@ class ItemScrollController {
|
|||||||
|
|
||||||
void _attach(_ScrollablePositionedListState scrollableListState) {
|
void _attach(_ScrollablePositionedListState scrollableListState) {
|
||||||
assert(
|
assert(
|
||||||
_scrollableListState == null, '_scrollableListState needs to be null');
|
_scrollableListState == null,
|
||||||
|
'''ScrollController must not be attached to multiple ScrollablePositionedLists.''',
|
||||||
|
);
|
||||||
_scrollableListState = scrollableListState;
|
_scrollableListState = scrollableListState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,11 +292,12 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
|
|
||||||
bool _isTransitioning = false;
|
bool _isTransitioning = false;
|
||||||
|
|
||||||
|
AnimationController? _animationController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final ItemPosition? initialPosition =
|
final initialPosition = PageStorage.of(context).readState(context);
|
||||||
PageStorage.of(context).readState(context);
|
|
||||||
primary
|
primary
|
||||||
..target = initialPosition?.index ?? widget.initialScrollIndex
|
..target = initialPosition?.index ?? widget.initialScrollIndex
|
||||||
..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment;
|
..alignment = initialPosition?.itemLeadingEdge ?? widget.initialAlignment;
|
||||||
@@ -301,6 +321,7 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
.removeListener(_updatePositions);
|
.removeListener(_updatePositions);
|
||||||
secondary.itemPositionsNotifier.itemPositions
|
secondary.itemPositionsNotifier.itemPositions
|
||||||
.removeListener(_updatePositions);
|
.removeListener(_updatePositions);
|
||||||
|
_animationController?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,84 +350,90 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => LayoutBuilder(
|
Widget build(BuildContext context) {
|
||||||
builder: (context, constraints) {
|
return LayoutBuilder(
|
||||||
final cacheExtent = _cacheExtent(constraints);
|
builder: (context, constraints) {
|
||||||
return GestureDetector(
|
final cacheExtent = _cacheExtent(constraints);
|
||||||
onPanDown: (_) => _stopScroll(canceled: true),
|
return GestureDetector(
|
||||||
excludeFromSemantics: true,
|
onPanDown: (_) => _stopScroll(canceled: true),
|
||||||
child: Stack(
|
excludeFromSemantics: true,
|
||||||
children: <Widget>[
|
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(
|
PostMountCallback(
|
||||||
key: primary.key,
|
key: secondary.key,
|
||||||
callback: startAnimationCallback,
|
callback: startAnimationCallback,
|
||||||
child: FadeTransition(
|
child: FadeTransition(
|
||||||
opacity: ReverseAnimation(opacity),
|
opacity: opacity,
|
||||||
child: NotificationListener<ScrollNotification>(
|
child: NotificationListener<ScrollNotification>(
|
||||||
onNotification: (_) => _isTransitioning,
|
onNotification: (_) => false,
|
||||||
child: PositionedList(
|
child: PositionedList(
|
||||||
keyboardDismissBehavior: widget.keyboardDismissBehavior,
|
|
||||||
itemBuilder: widget.itemBuilder,
|
itemBuilder: widget.itemBuilder,
|
||||||
separatorBuilder: widget.separatorBuilder,
|
separatorBuilder: widget.separatorBuilder,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
positionedIndex: primary.target,
|
itemPositionsNotifier: secondary.itemPositionsNotifier,
|
||||||
controller: primary.scrollController,
|
positionedIndex: secondary.target,
|
||||||
itemPositionsNotifier: primary.itemPositionsNotifier,
|
controller: secondary.scrollController,
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
cacheExtent: cacheExtent,
|
cacheExtent: cacheExtent,
|
||||||
alignment: primary.alignment,
|
alignment: secondary.alignment,
|
||||||
physics: widget.physics,
|
physics: widget.physics,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
addSemanticIndexes: widget.addSemanticIndexes,
|
addSemanticIndexes: widget.addSemanticIndexes,
|
||||||
semanticChildCount: widget.semanticChildCount,
|
semanticChildCount: widget.semanticChildCount,
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||||
findChildIndexCallback: widget.findChildIndexCallback,
|
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(
|
double _cacheExtent(BoxConstraints constraints) => max(
|
||||||
constraints.maxHeight * _screenScrollCount,
|
(widget.scrollDirection == Axis.vertical
|
||||||
|
? constraints.maxHeight
|
||||||
|
: constraints.maxWidth) *
|
||||||
|
_screenScrollCount,
|
||||||
widget.minCacheExtent ?? 0,
|
widget.minCacheExtent ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -434,16 +461,19 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
index = widget.itemCount - 1;
|
index = widget.itemCount - 1;
|
||||||
}
|
}
|
||||||
if (_isTransitioning) {
|
if (_isTransitioning) {
|
||||||
|
final scrollCompleter = Completer<void>();
|
||||||
_stopScroll(canceled: true);
|
_stopScroll(canceled: true);
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||||
_startScroll(
|
await _startScroll(
|
||||||
index: index,
|
index: index,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
curve: curve,
|
curve: curve,
|
||||||
opacityAnimationWeights: opacityAnimationWeights,
|
opacityAnimationWeights: opacityAnimationWeights,
|
||||||
);
|
);
|
||||||
|
scrollCompleter.complete();
|
||||||
});
|
});
|
||||||
|
await scrollCompleter.future;
|
||||||
} else {
|
} else {
|
||||||
await _startScroll(
|
await _startScroll(
|
||||||
index: index,
|
index: index,
|
||||||
@@ -486,10 +516,11 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
startAnimationCallback = () {
|
startAnimationCallback = () {
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
startAnimationCallback = () {};
|
startAnimationCallback = () {};
|
||||||
|
_animationController?.dispose();
|
||||||
opacity.parent = _opacityAnimation(opacityAnimationWeights).animate(
|
_animationController =
|
||||||
AnimationController(vsync: this, duration: duration)..forward(),
|
AnimationController(vsync: this, duration: duration)..forward();
|
||||||
);
|
opacity.parent = _opacityAnimation(opacityAnimationWeights)
|
||||||
|
.animate(_animationController!);
|
||||||
secondary.scrollController.jumpTo(-direction *
|
secondary.scrollController.jumpTo(-direction *
|
||||||
(_screenScrollCount *
|
(_screenScrollCount *
|
||||||
primary.scrollController.position.viewportDimension -
|
primary.scrollController.position.viewportDimension -
|
||||||
@@ -532,17 +563,19 @@ class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
if (mounted) {
|
||||||
if (opacity.value >= 0.5) {
|
setState(() {
|
||||||
// Secondary [ListView] is more visible than the primary; make it the
|
if (opacity.value >= 0.5) {
|
||||||
// new primary.
|
// Secondary [ListView] is more visible than the primary; make it the
|
||||||
final temp = primary;
|
// new primary.
|
||||||
primary = secondary;
|
final temp = primary;
|
||||||
secondary = temp;
|
primary = secondary;
|
||||||
}
|
secondary = temp;
|
||||||
_isTransitioning = false;
|
}
|
||||||
opacity.parent = const AlwaysStoppedAnimation<double>(0);
|
_isTransitioning = false;
|
||||||
});
|
opacity.parent = const AlwaysStoppedAnimation<double>(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Animatable<double> _opacityAnimation(List<double> opacityAnimationWeights) {
|
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
|
// 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.
|
||||||
|
|
||||||
// ignore_for_file: lines_longer_than_80_chars
|
|
||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
@@ -15,7 +13,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
/// Version of [Viewport] with some modifications to how extents are
|
/// Version of [Viewport] with some modifications to how extents are
|
||||||
/// computed to allow scroll extents outside 0 to 1. See [Viewport]
|
/// computed to allow scroll extents outside 0 to 1. See [Viewport]
|
||||||
/// for more information.
|
/// for more information.
|
||||||
/// description
|
/// {@endtemplate}
|
||||||
class UnboundedViewport extends Viewport {
|
class UnboundedViewport extends Viewport {
|
||||||
/// {@macro unbounded_viewport}
|
/// {@macro unbounded_viewport}
|
||||||
UnboundedViewport({
|
UnboundedViewport({
|
||||||
@@ -37,15 +35,16 @@ class UnboundedViewport extends Viewport {
|
|||||||
double get anchor => _anchor;
|
double get anchor => _anchor;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderViewport createRenderObject(BuildContext context) =>
|
RenderViewport createRenderObject(BuildContext context) {
|
||||||
UnboundedRenderViewport(
|
return UnboundedRenderViewport(
|
||||||
axisDirection: axisDirection,
|
axisDirection: axisDirection,
|
||||||
crossAxisDirection: crossAxisDirection ??
|
crossAxisDirection: crossAxisDirection ??
|
||||||
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
|
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
|
||||||
anchor: anchor,
|
anchor: anchor,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
cacheExtent: cacheExtent,
|
cacheExtent: cacheExtent,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A render object that is bigger on the inside.
|
/// A render object that is bigger on the inside.
|
||||||
@@ -137,14 +136,20 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
if (center == null) {
|
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;
|
_minScrollExtent = 0.0;
|
||||||
_maxScrollExtent = 0.0;
|
_maxScrollExtent = 0.0;
|
||||||
_hasVisualOverflow = false;
|
_hasVisualOverflow = false;
|
||||||
offset.applyContentDimensions(0, 0);
|
offset.applyContentDimensions(0, 0);
|
||||||
return;
|
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 mainAxisExtent;
|
||||||
late double crossAxisExtent;
|
late double crossAxisExtent;
|
||||||
@@ -186,7 +191,7 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
} while (count < _maxLayoutCycles);
|
} while (count < _maxLayoutCycles);
|
||||||
assert(() {
|
assert(() {
|
||||||
if (count >= _maxLayoutCycles) {
|
if (count >= _maxLayoutCycles) {
|
||||||
assert(count != 1, 'count not equal to 1');
|
assert(count != 1);
|
||||||
throw FlutterError(
|
throw FlutterError(
|
||||||
'A RenderViewport exceeded its maximum number of layout cycles.\n'
|
'A RenderViewport exceeded its maximum number of layout cycles.\n'
|
||||||
'RenderViewport render objects, during layout, can retry if either their '
|
'RenderViewport render objects, during layout, can retry if either their '
|
||||||
@@ -207,7 +212,7 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}(), 'count needs to be bigger than _maxLayoutCycles');
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
double _attemptLayout(
|
double _attemptLayout(
|
||||||
@@ -215,11 +220,11 @@ class UnboundedRenderViewport extends RenderViewport {
|
|||||||
double crossAxisExtent,
|
double crossAxisExtent,
|
||||||
double correctedOffset,
|
double correctedOffset,
|
||||||
) {
|
) {
|
||||||
assert(!mainAxisExtent.isNaN, 'assert mainAxisExtent.isNaN');
|
assert(!mainAxisExtent.isNaN, 'The main axis extent cannot be NaN.');
|
||||||
assert(mainAxisExtent >= 0.0, 'assert mainAxisExtent >= 0.0');
|
assert(mainAxisExtent >= 0.0, 'The main axis extent cannot be negative.');
|
||||||
assert(crossAxisExtent.isFinite, 'assert crossAxisExtent.isFinite');
|
assert(crossAxisExtent.isFinite, 'The cross axis extent must be finite.');
|
||||||
assert(crossAxisExtent >= 0.0, 'assert crossAxisExtent >= 0.0');
|
assert(crossAxisExtent >= 0.0, 'The cross axis extent cannot be negative.');
|
||||||
assert(correctedOffset.isFinite, 'assert correctedOffset.isFinite');
|
assert(correctedOffset.isFinite, 'The corrected offset must be finite.');
|
||||||
_minScrollExtent = 0.0;
|
_minScrollExtent = 0.0;
|
||||||
_maxScrollExtent = 0.0;
|
_maxScrollExtent = 0.0;
|
||||||
_hasVisualOverflow = false;
|
_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
|
/// A [StreamChannel] ancestor widget is required in order to provide the
|
||||||
/// information about the channels.
|
/// 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].
|
/// The UI is rendered based on the first ancestor of type [StreamChatTheme].
|
||||||
/// Modify it to change the widget's appearance.
|
/// Modify it to change the widget's appearance.
|
||||||
@@ -88,8 +88,10 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
this.threadBuilder,
|
this.threadBuilder,
|
||||||
this.onThreadTap,
|
this.onThreadTap,
|
||||||
this.dateDividerBuilder,
|
this.dateDividerBuilder,
|
||||||
this.scrollPhysics =
|
// we need to use ClampingScrollPhysics to avoid the list view to bounce
|
||||||
const ClampingScrollPhysics(), // we need to use ClampingScrollPhysics to avoid the list view to animate and break while loading
|
// 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.initialScrollIndex,
|
||||||
this.initialAlignment,
|
this.initialAlignment,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
@@ -113,6 +115,7 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
this.unreadMessagesSeparatorBuilder,
|
this.unreadMessagesSeparatorBuilder,
|
||||||
this.messageListController,
|
this.messageListController,
|
||||||
this.reverse = true,
|
this.reverse = true,
|
||||||
|
this.shrinkWrap = false,
|
||||||
this.paginationLimit = 20,
|
this.paginationLimit = 20,
|
||||||
this.paginationLoadingIndicatorBuilder,
|
this.paginationLoadingIndicatorBuilder,
|
||||||
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
|
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
@@ -133,6 +136,14 @@ class StreamMessageListView extends StatefulWidget {
|
|||||||
/// See [ScrollView.reverse].
|
/// See [ScrollView.reverse].
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
/// Limit used during pagination
|
/// Limit used during pagination
|
||||||
final int paginationLimit;
|
final int paginationLimit;
|
||||||
|
|
||||||
@@ -548,6 +559,7 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
physics: widget.scrollPhysics,
|
physics: widget.scrollPhysics,
|
||||||
itemScrollController: _scrollController,
|
itemScrollController: _scrollController,
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
|
shrinkWrap: widget.shrinkWrap,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
findChildIndexCallback: (Key key) {
|
findChildIndexCallback: (Key key) {
|
||||||
final indexedKey = key as IndexedKey;
|
final indexedKey = key as IndexedKey;
|
||||||
@@ -555,6 +567,10 @@ class _StreamMessageListViewState extends State<StreamMessageListView> {
|
|||||||
if (valueKey != null) {
|
if (valueKey != null) {
|
||||||
final index = messagesIndex[valueKey.value];
|
final index = messagesIndex[valueKey.value];
|
||||||
if (index != null) {
|
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;
|
return ((index + 2) * 2) - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-4
@@ -2,13 +2,12 @@
|
|||||||
// 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 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
const screenHeight = 400.0;
|
const screenHeight = 100.0;
|
||||||
const screenWidth = 400.0;
|
const screenWidth = 400.0;
|
||||||
const itemWidth = screenWidth / 10.0;
|
const itemWidth = screenWidth / 10.0;
|
||||||
const itemCount = 500;
|
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 {
|
testWidgets('List positioned with 0 at left', (WidgetTester tester) async {
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
||||||
@@ -172,7 +176,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 100')).dx, 0);
|
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(
|
expect(
|
||||||
itemPositionsListener.itemPositions.value
|
itemPositionsListener.itemPositions.value
|
||||||
@@ -196,6 +200,31 @@ void main() {
|
|||||||
1);
|
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',
|
testWidgets('padding test - centered sliver at left',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
|
|||||||
+53
@@ -360,4 +360,57 @@ void main() {
|
|||||||
.itemTrailingEdge,
|
.itemTrailingEdge,
|
||||||
1);
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -2,10 +2,9 @@
|
|||||||
// 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 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
const screenHeight = 400.0;
|
const screenHeight = 400.0;
|
||||||
|
|||||||
+277
-264
@@ -2,12 +2,12 @@
|
|||||||
// 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 'dart:async';
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
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';
|
||||||
|
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/scrollable_positioned_list.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
||||||
|
|
||||||
@@ -48,8 +48,10 @@ void main() {
|
|||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'''index needs to be bigger or equal to 0 and smallert than itemCount -1''');
|
index >= 0 && index <= itemCount - 1,
|
||||||
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
|
);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height:
|
height:
|
||||||
variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight,
|
variableHeight ? (itemHeight + (index % 13) * 5) : itemHeight,
|
||||||
@@ -71,6 +73,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 {
|
testWidgets('List positioned with 0 at top', (WidgetTester tester) async {
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
await setUpWidgetTest(tester, itemPositionsListener: itemPositionsListener);
|
||||||
@@ -394,11 +401,7 @@ void main() {
|
|||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener);
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
var fadeTransition = tester.widget<FadeTransition>(find
|
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
final initialOpacity = fadeTransition.opacity;
|
final initialOpacity = fadeTransition.opacity;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -407,11 +410,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(scrollDuration ~/ 2);
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
fadeTransition = tester.widget<FadeTransition>(find
|
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity, initialOpacity);
|
expect(fadeTransition.opacity, initialOpacity);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -456,10 +455,6 @@ void main() {
|
|||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
||||||
|
|
||||||
final fadeTransitionFinder = find.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition));
|
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
itemScrollController.scrollTo(index: 100, duration: scrollDuration));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@@ -533,26 +528,14 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
|
||||||
.widget<FadeTransition>(find
|
closeTo(0, 0.01),
|
||||||
.descendant(
|
);
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last)
|
|
||||||
.opacity
|
|
||||||
.value,
|
|
||||||
closeTo(0, 0.01));
|
|
||||||
await tester.pump(scrollDuration + scrollDurationTolerance);
|
await tester.pump(scrollDuration + scrollDurationTolerance);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
|
||||||
.widget<FadeTransition>(find
|
closeTo(1, 0.01),
|
||||||
.descendant(
|
);
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last)
|
|
||||||
.opacity
|
|
||||||
.value,
|
|
||||||
closeTo(1, 0.01));
|
|
||||||
|
|
||||||
expect(find.text('Item 0'), findsOneWidget);
|
expect(find.text('Item 0'), findsOneWidget);
|
||||||
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 0')).dy, 0);
|
||||||
@@ -610,15 +593,9 @@ void main() {
|
|||||||
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
||||||
expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight);
|
expect(tester.getBottomLeft(find.text('Item 19')).dy, screenHeight);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester.widget<FadeTransition>(fadeTransitionFinder.last).opacity.value,
|
||||||
.widget<FadeTransition>(find
|
closeTo(0.5, 0.01),
|
||||||
.descendant(
|
);
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last)
|
|
||||||
.opacity
|
|
||||||
.value,
|
|
||||||
closeTo(0.5, 0.01));
|
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
@@ -899,11 +876,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 9')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 9')).dy, 0);
|
||||||
final fadeTransition = tester.widget<FadeTransition>(find
|
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity.value, 1.0);
|
expect(fadeTransition.opacity.value, 1.0);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -923,21 +896,12 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
expect(tester.getTopLeft(find.text('Item 10')).dy, 0);
|
||||||
final fadeTransition = tester.widget<FadeTransition>(find
|
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity.value, 1.0);
|
expect(fadeTransition.opacity.value, 1.0);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
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 {
|
testWidgets('Scroll to 0 stop before half way', (WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
await setUpWidgetTest(tester, itemScrollController: itemScrollController);
|
||||||
@@ -1022,14 +986,13 @@ void main() {
|
|||||||
itemScrollController.scrollTo(index: 0, duration: scrollDuration));
|
itemScrollController.scrollTo(index: 0, duration: scrollDuration));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
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.tap(find.byType(ScrollablePositionedList));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(tester.getTopLeft(find.text('Item 9')).dy, closeTo(0, tolerance));
|
expect(tester.getTopLeft(find.text('Item 90')).dy, 0);
|
||||||
final fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
expect(fadeTransitionFinder, findsNWidgets(1));
|
||||||
expect(fadeTransition.opacity.value, 1.0);
|
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
@@ -1098,6 +1061,34 @@ void main() {
|
|||||||
expect(find.text('Item 100'), findsNothing);
|
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',
|
testWidgets('Scroll to 250, scroll to 100, scroll to 0 half way',
|
||||||
(WidgetTester tester) async {
|
(WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
@@ -1145,34 +1136,35 @@ void main() {
|
|||||||
}, skip: true);
|
}, skip: true);
|
||||||
|
|
||||||
testWidgets(
|
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 {
|
(WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
await setUpWidgetTest(tester,
|
await setUpWidgetTest(tester,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener);
|
itemPositionsListener: itemPositionsListener);
|
||||||
|
|
||||||
itemScrollController.jumpTo(index: 400, alignment: 1);
|
itemScrollController.jumpTo(index: 400, alignment: 1);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final listFinder = find.byType(ScrollablePositionedList);
|
final listFinder = find.byType(ScrollablePositionedList);
|
||||||
|
|
||||||
await tester.drag(listFinder, const Offset(0, -screenHeight));
|
await tester.drag(listFinder, const Offset(0, -screenHeight));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
unawaited(itemScrollController.scrollTo(
|
unawaited(itemScrollController.scrollTo(
|
||||||
index: 100, alignment: 1, duration: scrollDuration));
|
index: 100, alignment: 1, duration: scrollDuration));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
unawaited(itemScrollController.scrollTo(
|
unawaited(itemScrollController.scrollTo(
|
||||||
index: 400, alignment: 1, duration: scrollDuration));
|
index: 400, alignment: 1, duration: scrollDuration));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final itemFinder = find.text('Item 399');
|
final itemFinder = find.text('Item 399');
|
||||||
expect(itemFinder, findsOneWidget);
|
expect(itemFinder, findsOneWidget);
|
||||||
expect(tester.getBottomLeft(itemFinder).dy, screenHeight);
|
expect(tester.getBottomLeft(itemFinder).dy, screenHeight);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('physics', (WidgetTester tester) async {
|
testWidgets('physics', (WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
@@ -1664,70 +1656,71 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
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 {
|
(WidgetTester tester) async {
|
||||||
final itemPositionsListener = ItemPositionsListener.create();
|
final itemPositionsListener = ItemPositionsListener.create();
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
|
|
||||||
tester.binding.window.devicePixelRatioTestValue = 1.0;
|
tester.binding.window.devicePixelRatioTestValue = 1.0;
|
||||||
tester.binding.window.physicalSizeTestValue =
|
tester.binding.window.physicalSizeTestValue =
|
||||||
const Size(screenWidth, screenHeight);
|
const Size(screenWidth, screenHeight);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: PageView(
|
home: PageView(
|
||||||
children: [
|
children: [
|
||||||
KeyedSubtree(
|
KeyedSubtree(
|
||||||
key: const PageStorageKey('key'),
|
key: const PageStorageKey('key'),
|
||||||
child: ScrollablePositionedList.builder(
|
child: ScrollablePositionedList.builder(
|
||||||
itemCount: defaultItemCount,
|
itemCount: defaultItemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) => SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
|
),
|
||||||
|
itemPositionsListener: itemPositionsListener,
|
||||||
),
|
),
|
||||||
itemPositionsListener: itemPositionsListener,
|
|
||||||
),
|
),
|
||||||
),
|
const Center(
|
||||||
const Center(
|
child: Text('Test'),
|
||||||
child: Text('Test'),
|
)
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
itemScrollController.jumpTo(index: 9);
|
itemScrollController.jumpTo(index: 9);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(tester.getBottomRight(find.text('Item 9')).dy, itemHeight);
|
expect(tester.getBottomRight(find.text('Item 9')).dy, itemHeight);
|
||||||
|
|
||||||
await tester.drag(
|
await tester.drag(
|
||||||
find.byType(ScrollablePositionedList), const Offset(0, -itemHeight));
|
find.byType(ScrollablePositionedList), const Offset(0, -itemHeight));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final item9Bottom = tester.getBottomRight(find.text('Item 9')).dy;
|
final item9Bottom = tester.getBottomRight(find.text('Item 9')).dy;
|
||||||
expect(item9Bottom, lessThan(itemHeight));
|
expect(item9Bottom, lessThan(itemHeight));
|
||||||
|
|
||||||
await tester.drag(find.byType(PageView), const Offset(-500, 0));
|
await tester.drag(find.byType(PageView), const Offset(-500, 0));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
await tester.drag(find.byType(PageView), const Offset(500, 0));
|
await tester.drag(find.byType(PageView), const Offset(500, 0));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(tester.getBottomRight(find.text('Item 9')).dy, item9Bottom);
|
expect(tester.getBottomRight(find.text('Item 9')).dy, item9Bottom);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
itemPositionsListener.itemPositions.value
|
itemPositionsListener.itemPositions.value
|
||||||
.firstWhere((position) => position.index == 9)
|
.firstWhere((position) => position.index == 9)
|
||||||
.itemLeadingEdge,
|
.itemLeadingEdge,
|
||||||
-(itemHeight / screenHeight) / 2);
|
-(itemHeight / screenHeight) / 2);
|
||||||
expect(
|
expect(
|
||||||
itemPositionsListener.itemPositions.value
|
itemPositionsListener.itemPositions.value
|
||||||
.firstWhere((position) => position.index == 9)
|
.firstWhere((position) => position.index == 9)
|
||||||
.itemTrailingEdge,
|
.itemTrailingEdge,
|
||||||
(itemHeight / screenHeight) / 2);
|
(itemHeight / screenHeight) / 2);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('List with no items', (WidgetTester tester) async {
|
testWidgets('List with no items', (WidgetTester tester) async {
|
||||||
final itemScrollController = ItemScrollController();
|
final itemScrollController = ItemScrollController();
|
||||||
@@ -1751,21 +1744,24 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
initialScrollIndex: min(100, itemCount),
|
initialScrollIndex: min(100, itemCount),
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'index not bigger than 0 and smaller than itemCount - 1');
|
index >= 0 && index <= itemCount - 1,
|
||||||
return SizedBox(
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
height: itemHeight,
|
);
|
||||||
child: Text('Item $index'),
|
return SizedBox(
|
||||||
);
|
height: itemHeight,
|
||||||
},
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1795,19 +1791,22 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
initialScrollIndex: min(100, itemCount - 1),
|
initialScrollIndex: min(100, itemCount - 1),
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'index not bigger than 0 and smaller than itemCount -1');
|
index >= 0 && index <= itemCount - 1,
|
||||||
return SizedBox(
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
height: itemHeight,
|
);
|
||||||
child: Text('Item $index'),
|
return SizedBox(
|
||||||
);
|
height: itemHeight,
|
||||||
},
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1834,19 +1833,22 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
initialScrollIndex: itemCount - 1,
|
initialScrollIndex: itemCount - 1,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
assert(index >= 0 && index <= itemCount - 1,
|
assert(
|
||||||
'index not bigger than 0 and smaller than itemCount -1');
|
index >= 0 && index <= itemCount - 1,
|
||||||
return SizedBox(
|
'index must be in the range of 0 to itemCount - 1',
|
||||||
height: itemHeight,
|
);
|
||||||
child: Text('Item $index'),
|
return SizedBox(
|
||||||
);
|
height: itemHeight,
|
||||||
},
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1878,11 +1880,7 @@ void main() {
|
|||||||
minCacheExtent: 10,
|
minCacheExtent: 10,
|
||||||
);
|
);
|
||||||
|
|
||||||
var fadeTransition = tester.widget<FadeTransition>(find
|
var fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
final initialOpacity = fadeTransition.opacity;
|
final initialOpacity = fadeTransition.opacity;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -1891,11 +1889,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(scrollDuration ~/ 2);
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
fadeTransition = tester.widget<FadeTransition>(find
|
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity, initialOpacity);
|
expect(fadeTransition.opacity, initialOpacity);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -1914,11 +1908,9 @@ void main() {
|
|||||||
minCacheExtent: itemHeight * 200,
|
minCacheExtent: itemHeight * 200,
|
||||||
);
|
);
|
||||||
|
|
||||||
var fadeTransition = tester.widget<FadeTransition>(find
|
var fadeTransition = tester.widget<FadeTransition>(
|
||||||
.descendant(
|
fadeTransitionFinder,
|
||||||
of: find.byType(ScrollablePositionedList),
|
);
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
final initialOpacity = fadeTransition.opacity;
|
final initialOpacity = fadeTransition.opacity;
|
||||||
|
|
||||||
unawaited(
|
unawaited(
|
||||||
@@ -1927,11 +1919,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(scrollDuration ~/ 2);
|
await tester.pump(scrollDuration ~/ 2);
|
||||||
|
|
||||||
fadeTransition = tester.widget<FadeTransition>(find
|
fadeTransition = tester.widget<FadeTransition>(fadeTransitionFinder);
|
||||||
.descendant(
|
|
||||||
of: find.byType(ScrollablePositionedList),
|
|
||||||
matching: find.byType(FadeTransition))
|
|
||||||
.last);
|
|
||||||
expect(fadeTransition.opacity, initialOpacity);
|
expect(fadeTransition.opacity, initialOpacity);
|
||||||
|
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -1965,17 +1953,21 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: key,
|
valueListenable: key,
|
||||||
builder: (context, key, child) => Container(
|
builder: (context, key, child) {
|
||||||
key: key,
|
return Container(
|
||||||
child: ScrollablePositionedList.builder(
|
key: key,
|
||||||
itemCount: 200,
|
child: ScrollablePositionedList.builder(
|
||||||
itemScrollController: itemScrollController,
|
itemCount: 200,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemScrollController: itemScrollController,
|
||||||
height: itemHeight,
|
itemBuilder: (context, index) {
|
||||||
child: Text('Item $index'),
|
return SizedBox(
|
||||||
|
height: itemHeight,
|
||||||
|
child: Text('Item $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2054,15 +2046,19 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: key,
|
valueListenable: key,
|
||||||
builder: (context, key, child) => ScrollablePositionedList.builder(
|
builder: (context, key, child) {
|
||||||
key: key,
|
return ScrollablePositionedList.builder(
|
||||||
itemCount: 10,
|
key: key,
|
||||||
itemScrollController: itemScrollController,
|
itemCount: 10,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemScrollController: itemScrollController,
|
||||||
height: itemHeight,
|
itemBuilder: (context, index) {
|
||||||
child: Text('Item $index'),
|
return SizedBox(
|
||||||
),
|
height: itemHeight,
|
||||||
),
|
child: Text('Item $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2084,17 +2080,21 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: key,
|
valueListenable: key,
|
||||||
builder: (context, key, child) => Container(
|
builder: (context, key, child) {
|
||||||
key: key,
|
return Container(
|
||||||
child: ScrollablePositionedList.builder(
|
key: key,
|
||||||
itemCount: 100,
|
child: ScrollablePositionedList.builder(
|
||||||
itemScrollController: itemScrollController,
|
itemCount: 100,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemScrollController: itemScrollController,
|
||||||
height: itemHeight,
|
itemBuilder: (context, index) {
|
||||||
child: Text('Item $index'),
|
return SizedBox(
|
||||||
|
height: itemHeight,
|
||||||
|
child: Text('Item $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2124,18 +2124,22 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<Key>(
|
home: ValueListenableBuilder<Key>(
|
||||||
valueListenable: containerKey,
|
valueListenable: containerKey,
|
||||||
builder: (context, key, child) => Container(
|
builder: (context, key, child) {
|
||||||
key: key,
|
return Container(
|
||||||
child: ScrollablePositionedList.builder(
|
key: key,
|
||||||
key: scrollKey,
|
child: ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
key: scrollKey,
|
||||||
itemScrollController: itemScrollController,
|
itemCount: 100,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemScrollController: itemScrollController,
|
||||||
height: itemHeight,
|
itemBuilder: (context, index) {
|
||||||
child: Text('Item $index'),
|
return SizedBox(
|
||||||
|
height: itemHeight,
|
||||||
|
child: Text('Item $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2166,15 +2170,18 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<ItemScrollController>(
|
home: ValueListenableBuilder<ItemScrollController>(
|
||||||
valueListenable: itemScrollControllerListenable,
|
valueListenable: itemScrollControllerListenable,
|
||||||
builder: (context, itemScrollController, child) =>
|
builder: (context, itemScrollController, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
height: itemHeight,
|
return SizedBox(
|
||||||
child: Text('Item $index'),
|
height: itemHeight,
|
||||||
),
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2215,29 +2222,35 @@ void main() {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ValueListenableBuilder<ItemScrollController>(
|
child: ValueListenableBuilder<ItemScrollController>(
|
||||||
valueListenable: topItemScrollControllerListenable,
|
valueListenable: topItemScrollControllerListenable,
|
||||||
builder: (context, itemScrollController, child) =>
|
builder: (context, itemScrollController, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
height: itemHeight,
|
return SizedBox(
|
||||||
child: Text('Item $index'),
|
height: itemHeight,
|
||||||
),
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ValueListenableBuilder<ItemScrollController>(
|
child: ValueListenableBuilder<ItemScrollController>(
|
||||||
valueListenable: bottomItemScrollControllerListenable,
|
valueListenable: bottomItemScrollControllerListenable,
|
||||||
builder: (context, itemScrollController, child) =>
|
builder: (context, itemScrollController, child) {
|
||||||
ScrollablePositionedList.builder(
|
return ScrollablePositionedList.builder(
|
||||||
itemCount: 100,
|
itemCount: 100,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) {
|
||||||
height: itemHeight,
|
return SizedBox(
|
||||||
child: Text('Item $index'),
|
height: itemHeight,
|
||||||
),
|
child: Text('Item $index'),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
+31
-30
@@ -2,10 +2,9 @@
|
|||||||
// 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 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/scrollable_positioned_list.dart';
|
||||||
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
import 'package:stream_chat_flutter/scrollable_positioned_list/src/scroll_view.dart';
|
||||||
|
|
||||||
@@ -497,20 +496,21 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.separated(
|
return ScrollablePositionedList.separated(
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) => SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
),
|
),
|
||||||
separatorBuilder: (context, index) => SizedBox(
|
separatorBuilder: (context, index) => SizedBox(
|
||||||
height: separatorHeight,
|
height: separatorHeight,
|
||||||
child: Text('Separator $index'),
|
child: Text('Separator $index'),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -538,20 +538,21 @@ void main() {
|
|||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: ValueListenableBuilder<int>(
|
home: ValueListenableBuilder<int>(
|
||||||
valueListenable: itemCount,
|
valueListenable: itemCount,
|
||||||
builder: (context, itemCount, child) =>
|
builder: (context, itemCount, child) {
|
||||||
ScrollablePositionedList.separated(
|
return ScrollablePositionedList.separated(
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemScrollController: itemScrollController,
|
itemScrollController: itemScrollController,
|
||||||
itemPositionsListener: itemPositionsListener,
|
itemPositionsListener: itemPositionsListener,
|
||||||
itemBuilder: (context, index) => SizedBox(
|
itemBuilder: (context, index) => SizedBox(
|
||||||
height: itemHeight,
|
height: itemHeight,
|
||||||
child: Text('Item $index'),
|
child: Text('Item $index'),
|
||||||
),
|
),
|
||||||
separatorBuilder: (context, index) => SizedBox(
|
separatorBuilder: (context, index) => SizedBox(
|
||||||
height: separatorHeight,
|
height: separatorHeight,
|
||||||
child: Text('Separator $index'),
|
child: Text('Separator $index'),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-2
@@ -2,10 +2,9 @@
|
|||||||
// 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 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.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/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
const screenHeight = 400.0;
|
const screenHeight = 400.0;
|
||||||
|
|||||||
+477
@@ -0,0 +1,477 @@
|
|||||||
|
// 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
+247
@@ -0,0 +1,247 @@
|
|||||||
|
// 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user