Commit f4904b14 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Refresh indicator overscroll (#5836)

* Added OverscrollIndicatorEdge et al

* RefreshIndicator only clamps its scrollable edge

* added a test

* Updated the test

* fixed lint-os

* fixed a typo

* Scrollable should restore its viewport dimensions when it reappears

* removed an accidental commit

* updated per review feedback
parent 36b093d6
...@@ -45,46 +45,34 @@ class OverscrollDemoState extends State<OverscrollDemo> { ...@@ -45,46 +45,34 @@ class OverscrollDemoState extends State<OverscrollDemo> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget body = new MaterialList(
type: MaterialListType.threeLine,
padding: const EdgeInsets.all(8.0),
scrollableKey: _scrollableKey,
children: _items.map((String item) {
return new ListItem(
isThreeLine: true,
leading: new CircleAvatar(child: new Text(item)),
title: new Text('This item represents $item.'),
subtitle: new Text('Even more additional list item information appears on line three.')
);
})
);
String indicatorTypeText; String indicatorTypeText;
switch(_type) { switch (_type) {
case IndicatorType.overscroll: case IndicatorType.overscroll:
indicatorTypeText = 'Over-scroll indicator'; indicatorTypeText = 'Over-scroll indicator';
break; break;
case IndicatorType.refresh:
indicatorTypeText = 'Refresh indicator';
break;
}
// The default ScrollConfiguration doesn't include the
// OverscrollIndicator. That's what we want, since this demo
// adds the OverscrollIndicator itself.
Widget body = new ScrollConfiguration(
child: new MaterialList(
type: MaterialListType.threeLine,
padding: const EdgeInsets.all(8.0),
scrollableKey: _scrollableKey,
children: _items.map((String item) {
return new ListItem(
isThreeLine: true,
leading: new CircleAvatar(child: new Text(item)),
title: new Text('This item represents $item.'),
subtitle: new Text('Even more additional list item information appears on line three.')
);
})
)
);
switch(_type) {
case IndicatorType.overscroll:
body = new OverscrollIndicator(child: body);
break;
case IndicatorType.refresh: case IndicatorType.refresh:
body = new RefreshIndicator( body = new RefreshIndicator(
key: _refreshIndicatorKey, key: _refreshIndicatorKey,
child: body,
refresh: refresh, refresh: refresh,
scrollableKey: _scrollableKey, scrollableKey: _scrollableKey,
location: RefreshIndicatorLocation.top location: RefreshIndicatorLocation.top,
child: body,
); );
indicatorTypeText = 'Refresh indicator';
break; break;
} }
......
...@@ -304,7 +304,7 @@ class _RecipePageState extends State<RecipePage> { ...@@ -304,7 +304,7 @@ class _RecipePageState extends State<RecipePage> {
) )
), ),
new ClampOverscrolls( new ClampOverscrolls(
value: true, edge: ScrollableEdge.both,
child: new ScrollableViewport( child: new ScrollableViewport(
scrollableKey: _scrollableKey, scrollableKey: _scrollableKey,
child: new RepaintBoundary( child: new RepaintBoundary(
......
...@@ -173,8 +173,29 @@ class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate { ...@@ -173,8 +173,29 @@ class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate {
@override @override
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.android); ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.android);
ScrollableEdge _overscrollIndicatorEdge(ScrollableEdge edge) {
switch (edge) {
case ScrollableEdge.leading:
return ScrollableEdge.trailing;
case ScrollableEdge.trailing:
return ScrollableEdge.leading;
case ScrollableEdge.both:
return ScrollableEdge.none;
case ScrollableEdge.none:
return ScrollableEdge.both;
}
return ScrollableEdge.both;
}
@override @override
Widget wrapScrollWidget(Widget scrollWidget) => new OverscrollIndicator(child: scrollWidget); Widget wrapScrollWidget(BuildContext context, Widget scrollWidget) {
// Only introduce an overscroll indicator for the edges of the scrollable
// that aren't already clamped.
return new OverscrollIndicator(
edge: _overscrollIndicatorEdge(ClampOverscrolls.of(context)?.edge),
child: scrollWidget
);
}
@override @override
bool updateShouldNotify(ScrollConfigurationDelegate old) => false; bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
......
...@@ -89,7 +89,9 @@ class _DropDownScrollConfigurationDelegate extends ScrollConfigurationDelegate { ...@@ -89,7 +89,9 @@ class _DropDownScrollConfigurationDelegate extends ScrollConfigurationDelegate {
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: platform); ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: platform);
@override @override
Widget wrapScrollWidget(Widget scrollWidget) => new ClampOverscrolls(value: true, child: scrollWidget); Widget wrapScrollWidget(BuildContext context, Widget scrollWidget) {
return new ClampOverscrolls(edge: ScrollableEdge.both, child: scrollWidget);
}
@override @override
bool updateShouldNotify(ScrollConfigurationDelegate old) => platform != old.platform; bool updateShouldNotify(ScrollConfigurationDelegate old) => platform != old.platform;
......
...@@ -50,7 +50,7 @@ class _Painter extends CustomPainter { ...@@ -50,7 +50,7 @@ class _Painter extends CustomPainter {
final double width = size.width; final double width = size.width;
final double height = size.height; final double height = size.height;
switch(scrollDirection) { switch (scrollDirection) {
case Axis.vertical: case Axis.vertical:
final double radius = width * _kSizeToRadius; final double radius = width * _kSizeToRadius;
final double centerX = width / 2.0; final double centerX = width / 2.0;
...@@ -97,9 +97,11 @@ class OverscrollIndicator extends StatefulWidget { ...@@ -97,9 +97,11 @@ class OverscrollIndicator extends StatefulWidget {
OverscrollIndicator({ OverscrollIndicator({
Key key, Key key,
this.scrollableKey, this.scrollableKey,
this.edge: ScrollableEdge.both,
this.child this.child
}) : super(key: key) { }) : super(key: key) {
assert(child != null); assert(child != null);
assert(edge != null);
} }
/// Identifies the [Scrollable] descendant of child that the overscroll /// Identifies the [Scrollable] descendant of child that the overscroll
...@@ -107,6 +109,9 @@ class OverscrollIndicator extends StatefulWidget { ...@@ -107,6 +109,9 @@ class OverscrollIndicator extends StatefulWidget {
/// descendant. /// descendant.
final Key scrollableKey; final Key scrollableKey;
/// Where the overscroll indicator should appear.
final ScrollableEdge edge;
/// The overscroll indicator will be stacked on top of this child. The /// The overscroll indicator will be stacked on top of this child. The
/// indicator will appear when child's [Scrollable] descendant is /// indicator will appear when child's [Scrollable] descendant is
/// over-scrolled. /// over-scrolled.
...@@ -167,10 +172,12 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> { ...@@ -167,10 +172,12 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> {
// Hide the indicator as soon as user starts scrolling in the reverse direction of overscroll. // Hide the indicator as soon as user starts scrolling in the reverse direction of overscroll.
if (_isReverseScroll(value)) { if (_isReverseScroll(value)) {
_hide(_kNormalHideDuration); _hide(_kNormalHideDuration);
} else { } else if (_isMatchingOverscrollEdge(value)) {
// Changing the animation's value causes an implicit setState(). // Changing the animation's value causes an implicit setState().
_dragPosition = details?.globalPosition ?? Point.origin; _dragPosition = details?.globalPosition ?? Point.origin;
_extentAnimation.value = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset; _extentAnimation.value = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset;
} else {
_hide(_kNormalHideDuration);
} }
} }
_updateState(scrollable); _updateState(scrollable);
...@@ -194,6 +201,20 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> { ...@@ -194,6 +201,20 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> {
((scrollOffset - _scrollOffset).abs() > kPixelScrollTolerance.distance); ((scrollOffset - _scrollOffset).abs() > kPixelScrollTolerance.distance);
} }
bool _isMatchingOverscrollEdge(double scrollOffset) {
switch (config.edge) {
case ScrollableEdge.both:
return true;
case ScrollableEdge.leading:
return scrollOffset < _minScrollOffset;
case ScrollableEdge.trailing:
return scrollOffset > _maxScrollOffset;
case ScrollableEdge.none:
return false;
}
return false;
}
bool _isReverseScroll(double scrollOffset) { bool _isReverseScroll(double scrollOffset) {
final double delta = _scrollOffset - scrollOffset; final double delta = _scrollOffset - scrollOffset;
return scrollOffset < _minScrollOffset ? delta < 0.0 : delta > 0.0; return scrollOffset < _minScrollOffset ? delta < 0.0 : delta > 0.0;
...@@ -208,7 +229,7 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> { ...@@ -208,7 +229,7 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> {
} }
final ScrollableState scrollable = notification.scrollable; final ScrollableState scrollable = notification.scrollable;
switch(notification.kind) { switch (notification.kind) {
case ScrollNotificationKind.started: case ScrollNotificationKind.started:
_onScrollStarted(scrollable); _onScrollStarted(scrollable);
break; break;
...@@ -256,9 +277,10 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> { ...@@ -256,9 +277,10 @@ class _OverscrollIndicatorState extends State<OverscrollIndicator> {
child: child child: child
); );
}, },
child: new ClampOverscrolls( child: new ClampOverscrolls.inherit(
context: context,
edge: config.edge,
child: config.child, child: config.child,
value: true
) )
) )
); );
......
...@@ -331,6 +331,18 @@ class RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -331,6 +331,18 @@ class RefreshIndicatorState extends State<RefreshIndicator> {
} }
} }
ScrollableEdge get _clampOverscrollsEdge {
switch (config.location) {
case RefreshIndicatorLocation.top:
return ScrollableEdge.leading;
case RefreshIndicatorLocation.bottom:
return ScrollableEdge.trailing;
case RefreshIndicatorLocation.both:
return ScrollableEdge.both;
}
return ScrollableEdge.none;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
...@@ -353,9 +365,10 @@ class RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -353,9 +365,10 @@ class RefreshIndicatorState extends State<RefreshIndicator> {
onPointerUp: _handlePointerUp, onPointerUp: _handlePointerUp,
child: new Stack( child: new Stack(
children: <Widget>[ children: <Widget>[
new ClampOverscrolls( new ClampOverscrolls.inherit(
context: context,
edge: _clampOverscrollsEdge,
child: config.child, child: config.child,
value: true
), ),
new Positioned( new Positioned(
top: _isIndicatorAtTop ? 0.0 : null, top: _isIndicatorAtTop ? 0.0 : null,
......
...@@ -609,10 +609,9 @@ class ScaffoldState extends State<Scaffold> { ...@@ -609,10 +609,9 @@ class ScaffoldState extends State<Scaffold> {
if ((scrollable.config.scrollDirection == Axis.vertical) && if ((scrollable.config.scrollDirection == Axis.vertical) &&
(config.scrollableKey == null || config.scrollableKey == scrollable.config.key)) { (config.scrollableKey == null || config.scrollableKey == scrollable.config.key)) {
double newScrollOffset = scrollable.scrollOffset; double newScrollOffset = scrollable.scrollOffset;
if (ClampOverscrolls.of(scrollable.context)) { final ClampOverscrolls clampOverscrolls = ClampOverscrolls.of(context);
ExtentScrollBehavior limits = scrollable.scrollBehavior; if (clampOverscrolls != null)
newScrollOffset = newScrollOffset.clamp(limits.minScrollOffset, limits.maxScrollOffset); newScrollOffset = clampOverscrolls.clampScrollOffset(scrollable);
}
if (_scrollOffset != newScrollOffset) { if (_scrollOffset != newScrollOffset) {
setState(() { setState(() {
_scrollOffsetDelta = _scrollOffset - newScrollOffset; _scrollOffsetDelta = _scrollOffset - newScrollOffset;
......
...@@ -13,39 +13,103 @@ import 'scrollable.dart'; ...@@ -13,39 +13,103 @@ import 'scrollable.dart';
/// scrolled to the given `scrollOffset`. /// scrolled to the given `scrollOffset`.
typedef Widget ViewportBuilder(BuildContext context, ScrollableState state, double scrollOffset); typedef Widget ViewportBuilder(BuildContext context, ScrollableState state, double scrollOffset);
/// A widget that controls whether [Scrollable] descendants will overscroll. /// A widget that controls whether viewport descendants will overscroll their contents.
/// Overscrolling is clamped at the beginning or end or both according to the
/// [edge] parameter.
/// ///
/// If `true`, the ClampOverscroll's [Scrollable] descendant will clamp its /// Scroll offset limits are defined by the enclosing Scrollable's [ScrollBehavior].
/// viewport's scrollOffsets to the [ScrollBehavior]'s min and max values.
/// In this case the Scrollable's scrollOffset will still over- and undershoot
/// the ScrollBehavior's limits, but the viewport itself will not.
class ClampOverscrolls extends InheritedWidget { class ClampOverscrolls extends InheritedWidget {
/// Creates a widget that controls whether [Scrollable] descendants will overscroll. /// Creates a widget that controls whether viewport descendants will overscroll
/// their contents.
/// ///
/// The [value] and [child] arguments must not be null. /// The [edge] and [child] arguments must not be null.
ClampOverscrolls({ ClampOverscrolls({
Key key, Key key,
@required this.value, this.edge: ScrollableEdge.none,
@required Widget child @required Widget child,
}) : super(key: key, child: child) { }) : super(key: key, child: child) {
assert(value != null); assert(edge != null);
assert(child != null); assert(child != null);
} }
/// Whether [Scrollable] descendants should clamp their viewport's /// Creates a widget that controls whether viewport descendants will overscroll
/// scrollOffset values when they are less than the [ScrollBehavior]'s minimum /// based on the given [edge] and the inherited ClampOverscrolls widget for
/// or greater than its maximum. /// the given [context]. For example if edge is ScrollableEdge.leading
final bool value; /// and a ClampOverscrolls ancestor exists that specified ScrollableEdge.trailing,
/// then this widget would clamp both scrollable edges.
///
/// The [context], [edge] and [child] arguments must not be null.
factory ClampOverscrolls.inherit({
Key key,
@required BuildContext context,
@required ScrollableEdge edge: ScrollableEdge.none,
@required Widget child
}) {
assert(context != null);
assert(edge != null);
assert(child != null);
// The child's clamped edge is the union of the given edge and the
// parent's clamped edge.
ScrollableEdge parentEdge = ClampOverscrolls.of(context)?.edge ?? ScrollableEdge.none;
ScrollableEdge childEdge = edge;
switch (parentEdge) {
case ScrollableEdge.leading:
if (edge == ScrollableEdge.trailing || edge == ScrollableEdge.both)
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.trailing:
if (edge == ScrollableEdge.leading || edge == ScrollableEdge.both)
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.both:
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.none:
break;
}
/// Whether a [Scrollable] widget within the given context should overscroll. return new ClampOverscrolls(
static bool of(BuildContext context) { key: key,
final ClampOverscrolls result = context.inheritFromWidgetOfExactType(ClampOverscrolls); edge: childEdge,
return result?.value ?? false; child: child
);
} }
/// If ClampOverscrolls is true, clamps the ScrollableState's scrollOffset to the /// Defines when viewport scrollOffsets are clamped in terms of the scrollDirection.
/// [ScrollBehavior] minimum and maximum values and then constructs the viewport /// If edge is `leading` the viewport's scrollOffset will be clamped at its minimum
/// with the clamped scrollOffset. ClampOverscrolls is reset to false for viewport /// value (often 0.0). If edge is `trailing` then the scrollOffset will be clamped
/// to its maximum value. If edge is `both` then both the leading and trailing
/// constraints are applied.
final ScrollableEdge edge;
/// Return the [scrollable]'s scrollOffset clamped according to [edge].
double clampScrollOffset(ScrollableState scrollable) {
final double scrollOffset = scrollable.scrollOffset;
final double minScrollOffset = scrollable.scrollBehavior.minScrollOffset;
final double maxScrollOffset = scrollable.scrollBehavior.maxScrollOffset;
switch (edge) {
case ScrollableEdge.both:
return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
case ScrollableEdge.leading:
return scrollOffset.clamp(minScrollOffset, double.INFINITY);
case ScrollableEdge.trailing:
return scrollOffset.clamp(double.NEGATIVE_INFINITY, maxScrollOffset);
case ScrollableEdge.none:
return scrollOffset;
}
return scrollOffset;
}
/// The closest instance of this class that encloses the given context.
static ClampOverscrolls of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ClampOverscrolls);
}
/// Clamps the new viewport's scroll offset according to the value of
/// `ClampOverscrolls.of(context).edge`.
///
/// The clamped overscroll edge is reset to [ScrollableEdge.none] for the viewport's
/// descendants. /// descendants.
/// ///
/// This utility function is typically used by [Scrollable.builder] callbacks. /// This utility function is typically used by [Scrollable.builder] callbacks.
...@@ -54,22 +118,23 @@ class ClampOverscrolls extends InheritedWidget { ...@@ -54,22 +118,23 @@ class ClampOverscrolls extends InheritedWidget {
// by the container and content size. But we don't know those until we // by the container and content size. But we don't know those until we
// layout the viewport, which happens after build phase. We need to rethink // layout the viewport, which happens after build phase. We need to rethink
// this. // this.
final bool clampOverscrolls = ClampOverscrolls.of(context); final ClampOverscrolls clampOverscrolls = ClampOverscrolls.of(context);
final double clampedScrollOffset = clampOverscrolls if (clampOverscrolls == null)
? state.scrollOffset.clamp(state.scrollBehavior.minScrollOffset, state.scrollBehavior.maxScrollOffset) return builder(context, state, state.scrollOffset);
: state.scrollOffset;
final double clampedScrollOffset = clampOverscrolls.clampScrollOffset(state);
Widget viewport = builder(context, state, clampedScrollOffset); Widget viewport = builder(context, state, clampedScrollOffset);
if (clampOverscrolls) if (clampOverscrolls.edge != ScrollableEdge.none)
viewport = new ClampOverscrolls(value: false, child: viewport); viewport = new ClampOverscrolls(edge: ScrollableEdge.none, child: viewport);
return viewport; return viewport;
} }
@override @override
bool updateShouldNotify(ClampOverscrolls old) => value != old.value; bool updateShouldNotify(ClampOverscrolls old) => edge != old.edge;
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('value: $value'); description.add('edge: $edge');
} }
} }
// Copyright 2016 The Chromium Authors. All rights reserved. /// Copyright 2016 The Chromium Authors. All rights reserved.
// 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.
...@@ -28,7 +28,7 @@ abstract class ScrollConfigurationDelegate { ...@@ -28,7 +28,7 @@ abstract class ScrollConfigurationDelegate {
/// Scrollable they create. It can be used to add widgets that wrap the /// Scrollable they create. It can be used to add widgets that wrap the
/// Scrollable, like scrollbars or overscroll indicators. By default the /// Scrollable, like scrollbars or overscroll indicators. By default the
/// [scrollWidget] parameter is returned unchanged. /// [scrollWidget] parameter is returned unchanged.
Widget wrapScrollWidget(Widget scrollWidget) => scrollWidget; Widget wrapScrollWidget(BuildContext context, Widget scrollWidget) => scrollWidget;
/// Overrides should return true if this ScrollConfigurationDelegate differs /// Overrides should return true if this ScrollConfigurationDelegate differs
/// from the provided old delegate in a way that requires rebuilding its /// from the provided old delegate in a way that requires rebuilding its
...@@ -87,7 +87,7 @@ class ScrollConfiguration extends InheritedWidget { ...@@ -87,7 +87,7 @@ class ScrollConfiguration extends InheritedWidget {
/// A utility function that calls [ScrollConfigurationDelegate.wrapScrollWidget]. /// A utility function that calls [ScrollConfigurationDelegate.wrapScrollWidget].
static Widget wrap(BuildContext context, Widget scrollWidget) { static Widget wrap(BuildContext context, Widget scrollWidget) {
return ScrollConfiguration.of(context).wrapScrollWidget(scrollWidget); return ScrollConfiguration.of(context).wrapScrollWidget(context, scrollWidget);
} }
@override @override
......
...@@ -19,6 +19,24 @@ import 'page_storage.dart'; ...@@ -19,6 +19,24 @@ import 'page_storage.dart';
import 'scroll_behavior.dart'; import 'scroll_behavior.dart';
import 'scroll_configuration.dart'; import 'scroll_configuration.dart';
/// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection.
enum ScrollableEdge {
/// The top and bottom of the scrollable if its scrollDirection is vertical
/// or the left and right if its scrollDirection is horizontal.
both,
/// Only the top of the scrollable if its scrollDirection is vertical,
/// or only the left if its scrollDirection is horizontal.
leading,
/// Only the bottom of the scrollable if its scroll-direction is vertical,
/// or only the right if its scrollDirection is horizontal.
trailing,
/// The overscroll indicator should not appear at all.
none,
}
/// The accuracy to which scrolling is computed. /// The accuracy to which scrolling is computed.
final Tolerance kPixelScrollTolerance = new Tolerance( final Tolerance kPixelScrollTolerance = new Tolerance(
// TODO(ianh): Handle the case of the device pixel ratio changing. // TODO(ianh): Handle the case of the device pixel ratio changing.
......
// Copyright 2016 The Chromium 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
// Assuming that the test container is 800x600. The height of the
// viewport's contents is 650.0, the top and bottom text children
// are 100 pixels high and top/left edge of both widgets are visible.
// The top of the bottom widget is at 550 (the top of the top widget
// is at 0). The top of the bottom widget is 500 when it has been
// scrolled completely into view.
Widget buildFrame(ScrollableEdge clampedEdge) {
return new ClampOverscrolls(
edge: clampedEdge,
child: new ScrollableViewport(
scrollableKey: new UniqueKey(),
child: new SizedBox(
height: 650.0,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new SizedBox(height: 100.0, child: new Text('top')),
new Flexible(child: new Container()),
new SizedBox(height: 100.0, child: new Text('bottom')),
]
)
)
)
);
}
void main() {
testWidgets('ClampOverscrolls', (WidgetTester tester) async {
// Scroll the target text widget by offset and then return its origin
// in global coordinates.
Future<Point> locationAfterScroll(String target, Offset offset) async {
await tester.scrollAt(tester.getTopLeft(find.text(target)), offset);
await tester.pump();
final RenderBox textBox = tester.renderObject(find.text(target));
final Point widgetOrigin = textBox.localToGlobal(Point.origin);
await tester.pump(const Duration(seconds: 1)); // Allow overscroll to settle
return new Future<Point>.value(widgetOrigin);
}
await tester.pumpWidget(buildFrame(ScrollableEdge.none));
Point origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, greaterThan(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, lessThan(500.0));
await tester.pumpWidget(buildFrame(ScrollableEdge.both));
origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, equals(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, equals(500.0));
await tester.pumpWidget(buildFrame(ScrollableEdge.leading));
origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, equals(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, lessThan(500.0));
await tester.pumpWidget(buildFrame(ScrollableEdge.trailing));
origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, greaterThan(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, equals(500.0));
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment