Commit 659bc226 authored by Ian Hickson's avatar Ian Hickson Committed by Adam Barth

Port RefreshIndicator to slivers (#8218)

This does not attempt to correct any logic, only to port it as written.

The API changed a bit to take into account what is newly available and
no longer available in the new world.
parent e9af570f
......@@ -20,14 +20,11 @@ class OverscrollDemo extends StatefulWidget {
class OverscrollDemoState extends State<OverscrollDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = new GlobalKey<RefreshIndicatorState>();
static final GlobalKey<ScrollableState> _scrollableKey = new GlobalKey<ScrollableState>();
static final List<String> _items = <String>[
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'
];
IndicatorType _type = IndicatorType.refresh;
Future<Null> refresh() {
Future<Null> _handleRefresh() {
Completer<Null> completer = new Completer<Null>();
new Timer(new Duration(seconds: 3), () { completer.complete(null); });
return completer.future.then((_) {
......@@ -45,62 +42,37 @@ class OverscrollDemoState extends State<OverscrollDemo> {
@override
Widget build(BuildContext context) {
Widget body = new Block( // ignore: DEPRECATED_MEMBER_USE
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.')
);
}).toList(),
);
String indicatorTypeText;
switch (_type) {
case IndicatorType.overscroll:
indicatorTypeText = 'Over-scroll indicator';
break;
case IndicatorType.refresh:
body = new RefreshIndicator(
key: _refreshIndicatorKey,
refresh: refresh,
scrollableKey: _scrollableKey,
location: RefreshIndicatorLocation.top,
child: body,
);
indicatorTypeText = 'Refresh indicator';
break;
}
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('$indicatorTypeText'),
title: new Text('Pull to refresh'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.refresh),
tooltip: 'Pull to refresh',
tooltip: 'Refresh',
onPressed: () {
setState(() {
_type = IndicatorType.refresh;
});
_refreshIndicatorKey.currentState.show();
}
),
new IconButton(
icon: new Icon(Icons.play_for_work),
tooltip: 'Over-scroll indicator',
onPressed: () {
setState(() {
_type = IndicatorType.overscroll;
});
}
)
]
),
body: body
body: new RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _handleRefresh,
child: new ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
String item = _items[index];
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.'),
);
},
),
),
);
}
}
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
......@@ -17,9 +18,6 @@ const double _kDragContainerExtentPercentage = 0.25;
// displacement; max displacement = _kDragSizeFactorLimit * displacement.
const double _kDragSizeFactorLimit = 1.5;
// When the scroll ends, the duration of the refresh indicator's animation
// to the RefreshIndicator's displacment.
const Duration _kIndicatorSnapDuration = const Duration(milliseconds: 150);
......@@ -33,43 +31,30 @@ const Duration _kIndicatorScaleDuration = const Duration(milliseconds: 200);
/// refresh. The returned [Future] must complete when the refresh operation is
/// finished.
///
/// Used by [RefreshIndicator.refresh].
/// Used by [RefreshIndicator.onRefresh].
typedef Future<Null> RefreshCallback();
/// Where the refresh indicator appears: top for over-scrolls at the
/// start of the scrollable, bottom for over-scrolls at the end.
enum RefreshIndicatorLocation {
/// The refresh indicator will appear at the top of the scrollable.
top,
/// The refresh indicator will appear at the bottom of the scrollable.
bottom,
}
// The state machine moves through these modes only when the scrollable
// identified by scrollableKey has been scrolled to its min or max limit.
enum _RefreshIndicatorMode {
drag, // Pointer is down.
armed, // Dragged far enough that an up event will run the refresh callback.
snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback.
dismiss, // Animating the indicator's fade-out.
}
enum _DismissTransition {
shrink, // Refresh callback completed, scale the indicator to 0.
slide, // No refresh, translate the indicator out of view.
drag, // Pointer is down.
armed, // Dragged far enough that an up event will run the onRefresh callback.
snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback.
done, // Animating the indicator's fade-out after refreshing.
canceled, // Animating the indicator's fade-out after not arming.
}
/// A widget that supports the Material "swipe to refresh" idiom.
///
/// When the child's vertical Scrollable descendant overscrolls, an
/// animated circular progress indicator is faded into view. When the scroll
/// ends, if the indicator has been dragged far enough for it to become
/// completely opaque, the refresh callback is called. The callback is
/// expected to update the scrollable's contents and then complete the Future
/// it returns. The refresh indicator disappears after the callback's
/// Future has completed.
/// When the child's [Scrollable2] descendant overscrolls, an animated circular
/// progress indicator is faded into view. When the scroll ends, if the
/// indicator has been dragged far enough for it to become completely opaque,
/// the [onRefresh] callback is called. The callback is expected to update the
/// scrollable's contents and then complete the [Future] it returns. The refresh
/// indicator disappears after the callback's [Future] has completed.
///
/// A [RefreshIndicator] can only be used with a vertical scroll view (the xxxxxxx.
///
/// See also:
///
......@@ -79,44 +64,33 @@ enum _DismissTransition {
class RefreshIndicator extends StatefulWidget {
/// Creates a refresh indicator.
///
/// The [refresh] and [child] arguments must be non-null. The default
/// The [onRefresh] and [child] arguments must be non-null. The default
/// [displacement] is 40.0 logical pixels.
RefreshIndicator({
Key key,
this.scrollableKey,
this.child,
this.displacement: 40.0,
this.refresh,
this.location: RefreshIndicatorLocation.top,
this.onRefresh,
this.color,
this.backgroundColor
}) : super(key: key) {
assert(child != null);
assert(refresh != null);
assert(location != null);
assert(onRefresh != null);
}
/// Identifies the [Scrollable] descendant of child that will cause the
/// refresh indicator to appear.
final GlobalKey<ScrollableState> scrollableKey;
/// The refresh indicator will be stacked on top of this child. The indicator
/// will appear when child's Scrollable descendant is over-scrolled.
final Widget child;
/// The distance from the child's top or bottom edge to where the refresh indicator
/// will settle. During the drag that exposes the refresh indicator, its actual
/// displacement may significantly exceed this value.
/// The distance from the child's top or bottom edge to where the refresh
/// indicator will settle. During the drag that exposes the refresh indicator,
/// its actual displacement may significantly exceed this value.
final double displacement;
/// A function that's called when the user has dragged the refresh indicator
/// far enough to demonstrate that they want the app to refresh. The returned
/// [Future] must complete when the refresh operation is finished.
final RefreshCallback refresh;
/// Where the refresh indicator should appear, [RefreshIndicatorLocation.top]
/// by default.
final RefreshIndicatorLocation location;
final RefreshCallback onRefresh;
/// The progress indicator's foreground color. The current theme's
/// [ThemeData.accentColor] by default.
......@@ -133,135 +107,180 @@ class RefreshIndicator extends StatefulWidget {
/// Contains the state for a [RefreshIndicator]. This class can be used to
/// programmatically show the refresh indicator, see the [show] method.
class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderStateMixin {
AnimationController _sizeController;
AnimationController _positionController;
AnimationController _scaleController;
Animation<double> _sizeFactor;
Animation<double> _positionFactor;
Animation<double> _scaleFactor;
Animation<double> _value;
Animation<Color> _valueColor;
double _dragOffset;
bool _isIndicatorAtTop = true;
_RefreshIndicatorMode _mode;
Future<Null> _pendingRefreshFuture;
bool _isIndicatorAtTop;
double _dragOffset;
@override
void initState() {
super.initState();
_sizeController = new AnimationController(vsync: this);
_sizeFactor = new Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit).animate(_sizeController);
_positionController = new AnimationController(vsync: this);
_positionFactor = new Tween<double>(
begin: 0.0,
end: _kDragSizeFactorLimit,
).animate(_positionController);
_value = new Tween<double>( // The "value" of the circular progress indicator during a drag.
begin: 0.0,
end: 0.75
).animate(_sizeController);
end: 0.75,
).animate(_positionController);
_scaleController = new AnimationController(vsync: this);
_scaleFactor = new Tween<double>(begin: 1.0, end: 0.0).animate(_scaleController);
_scaleFactor = new Tween<double>(
begin: 1.0,
end: 0.0,
).animate(_scaleController);
}
@override
void dependenciesChanged() {
final ThemeData theme = Theme.of(context);
_valueColor = new ColorTween(
begin: (config.color ?? theme.accentColor).withOpacity(0.0),
end: (config.color ?? theme.accentColor).withOpacity(1.0)
).animate(new CurvedAnimation(
parent: _positionController,
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)
));
super.dependenciesChanged();
}
@override
void dispose() {
_sizeController.dispose();
_positionController.dispose();
_scaleController.dispose();
super.dispose();
}
bool _isValidScrollable(ScrollableState scrollable) {
if (scrollable == null)
bool _handleScrollNotification(ScrollNotification2 notification) {
if (notification.depth != 0)
return false;
if (notification is ScrollStartNotification && notification.metrics.extentBefore == 0.0 &&
_mode == null && _start(notification.axisDirection)) {
setState(() {
_mode = _RefreshIndicatorMode.drag;
});
return false;
final Axis axis = scrollable.config.scrollDirection;
return axis == Axis.vertical && scrollable.scrollBehavior is ExtentScrollBehavior;
}
bool _isScrolledToLimit(ScrollableState scrollable) {
final double minScrollOffset = scrollable.scrollBehavior.minScrollOffset;
final double maxScrollOffset = scrollable.scrollBehavior.maxScrollOffset;
final double scrollOffset = scrollable.scrollOffset;
switch (config.location) {
case RefreshIndicatorLocation.top:
return scrollOffset <= minScrollOffset;
case RefreshIndicatorLocation.bottom:
return scrollOffset >= maxScrollOffset;
}
bool indicatorAtTopNow;
switch (notification.axisDirection) {
case AxisDirection.down:
indicatorAtTopNow = true;
break;
case AxisDirection.up:
indicatorAtTopNow = false;
break;
case AxisDirection.left:
case AxisDirection.right:
indicatorAtTopNow = null;
break;
}
if (indicatorAtTopNow != _isIndicatorAtTop) {
if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed)
_dismiss(_RefreshIndicatorMode.canceled);
} else if (notification is ScrollUpdateNotification) {
if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
if (notification.metrics.extentBefore > 0.0) {
_dismiss(_RefreshIndicatorMode.canceled);
} else {
_dragOffset -= notification.scrollDelta;
_checkDragOffset(notification.metrics.viewportDimension);
}
}
} else if (notification is OverscrollNotification) {
if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
_dragOffset -= notification.overscroll / 2.0;
_checkDragOffset(notification.metrics.viewportDimension);
}
} else if (notification is ScrollEndNotification) {
switch (_mode) {
case _RefreshIndicatorMode.armed:
_show();
break;
case _RefreshIndicatorMode.drag:
_dismiss(_RefreshIndicatorMode.canceled);
break;
default:
// do nothing
break;
}
}
return false;
}
double _overscrollDistance(ScrollableState scrollable) {
final double minScrollOffset = scrollable.scrollBehavior.minScrollOffset;
final double maxScrollOffset = scrollable.scrollBehavior.maxScrollOffset;
final double scrollOffset = scrollable.scrollOffset;
switch (config.location) {
case RefreshIndicatorLocation.top:
return scrollOffset <= minScrollOffset ? -_dragOffset : 0.0;
case RefreshIndicatorLocation.bottom:
return scrollOffset >= maxScrollOffset ? _dragOffset : 0.0;
bool _handleGlowNotification(OverscrollIndicatorNotification notification) {
if (notification.depth != 0 || !notification.leading)
return false;
if (_mode == _RefreshIndicatorMode.drag) {
notification.disallowGlow();
return true;
}
return 0.0;
return false;
}
void _handlePointerDown(PointerDownEvent event) {
if (_mode != null)
return;
final ScrollableState scrollable = config.scrollableKey.currentState;
if (!_isValidScrollable(scrollable) || !_isScrolledToLimit(scrollable))
return;
bool _start(AxisDirection direction) {
assert(_mode == null);
assert(_isIndicatorAtTop == null);
assert(_dragOffset == null);
switch (direction) {
case AxisDirection.down:
_isIndicatorAtTop = true;
break;
case AxisDirection.up:
_isIndicatorAtTop = false;
break;
case AxisDirection.left:
case AxisDirection.right:
_isIndicatorAtTop = null;
// we do not support horizontal scroll views.
return false;
}
_dragOffset = 0.0;
_scaleController.value = 0.0;
_sizeController.value = 0.0;
setState(() {
_mode = _RefreshIndicatorMode.drag;
});
_positionController.value = 0.0;
return true;
}
void _handlePointerMove(PointerMoveEvent event) {
if (_mode != _RefreshIndicatorMode.drag && _mode != _RefreshIndicatorMode.armed)
return;
final ScrollableState scrollable = config.scrollableKey?.currentState;
if (!_isValidScrollable(scrollable))
return;
final double dragOffsetDelta = scrollable.pixelOffsetToScrollOffset(event.delta.dy);
_dragOffset += dragOffsetDelta / 2.0;
if (_dragOffset.abs() < kPixelScrollTolerance.distance)
return;
final double containerExtent = scrollable.scrollBehavior.containerExtent;
final double overscroll = _overscrollDistance(scrollable);
if (overscroll > 0.0) {
final double newValue = overscroll / (containerExtent * _kDragContainerExtentPercentage);
_sizeController.value = newValue.clamp(0.0, 1.0);
final bool newIsAtTop = _dragOffset < 0;
if (_isIndicatorAtTop != newIsAtTop) {
setState(() {
_isIndicatorAtTop = newIsAtTop;
});
}
}
// No setState() here because this doesn't cause a visual change.
_mode = _valueColor.value.alpha == 0xFF ? _RefreshIndicatorMode.armed : _RefreshIndicatorMode.drag;
void _checkDragOffset(double containerExtent) {
assert(_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed);
double newValue = _dragOffset / (containerExtent * _kDragContainerExtentPercentage);
if (_mode == _RefreshIndicatorMode.armed)
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
_positionController.value = newValue.clamp(0.0, 1.0); // this triggers various rebuilds
if (_mode == _RefreshIndicatorMode.drag && _valueColor.value.alpha == 0xFF)
_mode = _RefreshIndicatorMode.armed;
}
// Stop showing the refresh indicator
Future<Null> _dismiss(_DismissTransition transition) async {
// This can only be called from _show() when refreshing
// and _handlePointerUp when dragging.
// Stop showing the refresh indicator.
Future<Null> _dismiss(_RefreshIndicatorMode newMode) async {
// This can only be called from _show() when refreshing and
// _handleScrollNotification in response to a ScrollEndNotification or
// direction change.
assert(newMode == _RefreshIndicatorMode.canceled || newMode == _RefreshIndicatorMode.done);
setState(() {
_mode = _RefreshIndicatorMode.dismiss;
_mode = newMode;
});
switch(transition) {
case _DismissTransition.shrink:
await _sizeController.animateTo(0.0, duration: _kIndicatorScaleDuration);
break;
case _DismissTransition.slide:
switch (_mode) {
case _RefreshIndicatorMode.done:
await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
break;
case _RefreshIndicatorMode.canceled:
await _positionController.animateTo(0.0, duration: _kIndicatorScaleDuration);
break;
default:
assert(false);
}
if (mounted && _mode == _RefreshIndicatorMode.dismiss) {
if (mounted && _mode == newMode) {
_dragOffset = null;
_isIndicatorAtTop = null;
setState(() {
_mode = null;
});
......@@ -274,125 +293,110 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
Completer<Null> completer = new Completer<Null>();
_pendingRefreshFuture = completer.future;
_mode = _RefreshIndicatorMode.snap;
_sizeController
_positionController
.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration)
.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.snap) {
assert(config.refresh != null);
assert(config.onRefresh != null);
setState(() {
// Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
});
config.refresh().whenComplete(() {
config.onRefresh().whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
completer.complete();
_dismiss(_DismissTransition.slide);
_dismiss(_RefreshIndicatorMode.done);
}
});
}
});
}
void _handlePointerUp(PointerEvent event) {
switch (_mode) {
case _RefreshIndicatorMode.armed:
_show();
break;
case _RefreshIndicatorMode.drag:
_dismiss(_DismissTransition.shrink);
break;
default:
// do nothing
break;
}
}
/// Show the refresh indicator and run the refresh callback as if it had
/// been started interactively. If this method is called while the refresh
/// callback is running, it quietly does nothing.
///
/// Creating the RefreshIndicator with a [GlobalKey<RefreshIndicatorState>]
/// Creating the [RefreshIndicator] with a [GlobalKey<RefreshIndicatorState>]
/// makes it possible to refer to the [RefreshIndicatorState].
Future<Null> show() {
///
/// The future returned from this method completes when the [onRefresh]
/// callback's future completes.
///
/// If you await the future returned by this function from a [State], you
/// should check that the state is still [mounted] before calling [setState].
///
/// When initiated in this manner, the refresh indicator is independent of any
/// actual scroll view. It defaults to showing the indicator at the top. To
/// show it at the bottom, set `atTop` to false.
Future<Null> show({ bool atTop: true }) {
if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) {
_sizeController.value = 0.0;
_scaleController.value = 0.0;
if (_mode == null)
_start(atTop ? AxisDirection.down : AxisDirection.up);
_show();
}
return _pendingRefreshFuture;
}
ScrollableEdge get _clampOverscrollsEdge {
switch (config.location) {
case RefreshIndicatorLocation.top:
return ScrollableEdge.leading;
case RefreshIndicatorLocation.bottom:
return ScrollableEdge.trailing;
}
return ScrollableEdge.none;
}
final GlobalKey _key = new GlobalKey();
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final bool showIndeterminateIndicator =
_mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.dismiss;
// Fully opaque when we've reached config.displacement.
_valueColor = new ColorTween(
begin: (config.color ?? theme.accentColor).withOpacity(0.0),
end: (config.color ?? theme.accentColor).withOpacity(1.0)
).animate(new CurvedAnimation(
parent: _sizeController,
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)
));
Widget child = new NotificationListener<ScrollNotification2>(
key: _key,
onNotification: _handleScrollNotification,
child: new NotificationListener<OverscrollIndicatorNotification>(
onNotification: _handleGlowNotification,
child: config.child,
),
);
if (_mode == null) {
assert(_dragOffset == null);
assert(_isIndicatorAtTop == null);
return child;
}
assert(_dragOffset != null);
assert(_isIndicatorAtTop != null);
return new Listener(
onPointerDown: _handlePointerDown,
onPointerMove: _handlePointerMove,
onPointerUp: _handlePointerUp,
child: new Stack(
children: <Widget>[
new ClampOverscrolls.inherit(
context: context,
edge: _clampOverscrollsEdge,
child: config.child,
final bool showIndeterminateIndicator =
_mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.done;
return new Stack(
children: <Widget>[
child,
new Positioned(
top: _isIndicatorAtTop ? 0.0 : null,
bottom: !_isIndicatorAtTop ? 0.0 : null,
left: 0.0,
right: 0.0,
child: new SizeTransition(
axisAlignment: _isIndicatorAtTop ? 1.0 : 0.0,
sizeFactor: _positionFactor, // this is what brings it down
child: new Container(
padding: _isIndicatorAtTop
? new EdgeInsets.only(top: config.displacement)
: new EdgeInsets.only(bottom: config.displacement),
alignment: _isIndicatorAtTop
? FractionalOffset.topCenter
: FractionalOffset.bottomCenter,
child: new ScaleTransition(
scale: _scaleFactor,
child: new AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget child) {
return new RefreshProgressIndicator(
value: showIndeterminateIndicator ? null : _value.value,
valueColor: _valueColor,
backgroundColor: config.backgroundColor,
);
},
),
),
),
),
new Positioned(
top: _isIndicatorAtTop ? 0.0 : null,
bottom: _isIndicatorAtTop ? null : 0.0,
left: 0.0,
right: 0.0,
child: new SizeTransition(
axisAlignment: _isIndicatorAtTop ? 1.0 : 0.0,
sizeFactor: _sizeFactor,
child: new Container(
padding: _isIndicatorAtTop
? new EdgeInsets.only(top: config.displacement)
: new EdgeInsets.only(bottom: config.displacement),
alignment: _isIndicatorAtTop
? FractionalOffset.bottomCenter
: FractionalOffset.topCenter,
child: new ScaleTransition(
scale: _scaleFactor,
child: new AnimatedBuilder(
animation: _sizeController,
builder: (BuildContext context, Widget child) {
return new RefreshProgressIndicator(
value: showIndeterminateIndicator ? null : _value.value,
valueColor: _valueColor,
backgroundColor: config.backgroundColor
);
}
)
)
)
)
)
]
)
),
],
);
}
}
......@@ -107,6 +107,9 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
}
}
Type _lastNotificationType;
final Map<bool, bool> _accepted = <bool, bool>{false: true, true: true};
bool _handleScrollNotification(ScrollNotification2 notification) {
if (notification.depth != 0)
return false;
......@@ -119,27 +122,35 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
} else {
assert(false);
}
bool isLeading = controller == _leadingController;
if (_lastNotificationType != OverscrollNotification) {
OverscrollIndicatorNotification confirmationNotification = new OverscrollIndicatorNotification(leading: isLeading);
confirmationNotification.dispatch(context);
_accepted[isLeading] = confirmationNotification._accepted;
}
assert(controller != null);
assert(notification.axis == config.axis);
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
controller.absorbImpact(notification.velocity.abs());
} else {
assert(notification.overscroll != 0.0);
if (notification.dragDetails != null) {
assert(notification.dragDetails.globalPosition != null);
final RenderBox renderer = notification.context.findRenderObject();
assert(renderer != null);
assert(renderer.hasSize);
final Size size = renderer.size;
final Point position = renderer.globalToLocal(notification.dragDetails.globalPosition);
switch (notification.axis) {
case Axis.horizontal:
controller.pull(notification.overscroll.abs(), size.width, position.y.clamp(0.0, size.height), size.height);
break;
case Axis.vertical:
controller.pull(notification.overscroll.abs(), size.height, position.x.clamp(0.0, size.width), size.width);
break;
if (_accepted[isLeading]) {
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
controller.absorbImpact(notification.velocity.abs());
} else {
assert(notification.overscroll != 0.0);
if (notification.dragDetails != null) {
assert(notification.dragDetails.globalPosition != null);
final RenderBox renderer = notification.context.findRenderObject();
assert(renderer != null);
assert(renderer.hasSize);
final Size size = renderer.size;
final Point position = renderer.globalToLocal(notification.dragDetails.globalPosition);
switch (notification.axis) {
case Axis.horizontal:
controller.pull(notification.overscroll.abs(), size.width, position.y.clamp(0.0, size.height), size.height);
break;
case Axis.vertical:
controller.pull(notification.overscroll.abs(), size.height, position.x.clamp(0.0, size.width), size.width);
break;
}
}
}
}
......@@ -149,6 +160,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
_trailingController.scrollEnd();
}
}
_lastNotificationType = notification.runtimeType;
return false;
}
......@@ -466,3 +478,24 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter {
|| oldDelegate.trailingController != trailingController;
}
}
class OverscrollIndicatorNotification extends Notification with ViewportNotificationMixin {
OverscrollIndicatorNotification({
this.leading,
});
final bool leading;
bool _accepted = true;
/// Call this method if the glow should be prevented.
void disallowGlow() {
_accepted = false;
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('side: ${leading ? "leading edge" : "trailing edge"}');
}
}
\ No newline at end of file
......@@ -54,7 +54,36 @@ class ScrollMetrics {
}
}
abstract class ScrollNotification2 extends LayoutChangedNotification {
/// Mixin for [Notification]s that track how many [RenderAbstractViewport] they
/// have bubbled through.
///
/// This is used by [ScrollNotification2] and [OverscrollIndicatorNotification].
abstract class ViewportNotificationMixin extends Notification {
/// The number of viewports that this notification has bubbled through.
///
/// Typically listeners only respond to notifications with a [depth] of zero.
///
/// Specifically, this is the number of [Widget]s representing
/// [RenderAbstractViewport] render objects through which this notification
/// has bubbled.
int get depth => _depth;
int _depth = 0;
@override
bool visitAncestor(Element element) {
if (element is RenderObjectElement && element.renderObject is RenderAbstractViewport)
_depth += 1;
return super.visitAncestor(element);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('depth: $depth (${ depth == 0 ? "local" : "remote"})');
}
}
abstract class ScrollNotification2 extends LayoutChangedNotification with ViewportNotificationMixin {
/// Creates a notification about scrolling.
ScrollNotification2({
@required Scrollable2State scrollable,
......@@ -75,29 +104,11 @@ abstract class ScrollNotification2 extends LayoutChangedNotification {
/// size of the viewport, for instance.
final BuildContext context;
/// The number of viewports that this notification has bubbled through.
///
/// Typically listeners only respond to notifications with a [depth] of zero.
///
/// Specifically, this is the number of [Widget]s representing
/// [RenderAbstractViewport] render objects through which this notification
/// has bubbled.
int get depth => _depth;
int _depth = 0;
@override
bool visitAncestor(Element element) {
if (element is RenderObjectElement && element.renderObject is RenderAbstractViewport)
_depth += 1;
return super.visitAncestor(element);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('metrics: $metrics');
description.add('depth: $depth (${ depth == 0 ? "local" : "remote"})');
}
}
......
......@@ -7,8 +7,6 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
bool refreshCalled = false;
Future<Null> refresh() {
......@@ -26,10 +24,8 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map((String item) {
return new SizedBox(
height: 200.0,
......@@ -52,15 +48,13 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
location: RefreshIndicatorLocation.bottom,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
reverse: true,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -75,18 +69,88 @@ void main() {
expect(refreshCalled, true);
});
testWidgets('RefreshIndicator - top - position', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
onRefresh: holdRefresh,
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X'),
),
],
),
),
);
await tester.fling(find.text('X'), const Offset(0.0, -300.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(tester.getCenter(find.byType(RefreshProgressIndicator)).y, lessThan(300.0));
});
testWidgets('RefreshIndicator - bottom - position', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
onRefresh: holdRefresh,
child: new ListView(
reverse: true,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X'),
),
],
),
),
);
await tester.fling(find.text('X'), const Offset(0.0, -300.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(tester.getCenter(find.byType(RefreshProgressIndicator)).y, greaterThan(300.0));
});
testWidgets('RefreshIndicator - no movement', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
onRefresh: refresh,
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X'),
),
],
),
),
);
// this fling is horizontal, not up or down
await tester.fling(find.text('X'), const Offset(1.0, 0.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, false);
});
testWidgets('RefreshIndicator - not enough', (WidgetTester tester) async {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -105,14 +169,12 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: holdRefresh, // this one never returns
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: holdRefresh, // this one never returns
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -147,14 +209,12 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -190,14 +250,12 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......
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