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 { ...@@ -20,14 +20,11 @@ class OverscrollDemo extends StatefulWidget {
class OverscrollDemoState extends State<OverscrollDemo> { class OverscrollDemoState extends State<OverscrollDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = new GlobalKey<RefreshIndicatorState>(); final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = new GlobalKey<RefreshIndicatorState>();
static final GlobalKey<ScrollableState> _scrollableKey = new GlobalKey<ScrollableState>();
static final List<String> _items = <String>[ static final List<String> _items = <String>[
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N' 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'
]; ];
IndicatorType _type = IndicatorType.refresh; Future<Null> _handleRefresh() {
Future<Null> refresh() {
Completer<Null> completer = new Completer<Null>(); Completer<Null> completer = new Completer<Null>();
new Timer(new Duration(seconds: 3), () { completer.complete(null); }); new Timer(new Duration(seconds: 3), () { completer.complete(null); });
return completer.future.then((_) { return completer.future.then((_) {
...@@ -45,62 +42,37 @@ class OverscrollDemoState extends State<OverscrollDemo> { ...@@ -45,62 +42,37 @@ class OverscrollDemoState extends State<OverscrollDemo> {
@override @override
Widget build(BuildContext context) { 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( return new Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
appBar: new AppBar( appBar: new AppBar(
title: new Text('$indicatorTypeText'), title: new Text('Pull to refresh'),
actions: <Widget>[ actions: <Widget>[
new IconButton( new IconButton(
icon: new Icon(Icons.refresh), icon: new Icon(Icons.refresh),
tooltip: 'Pull to refresh', tooltip: 'Refresh',
onPressed: () { onPressed: () {
setState(() { _refreshIndicatorKey.currentState.show();
_type = IndicatorType.refresh;
});
} }
), ),
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 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -17,9 +18,6 @@ const double _kDragContainerExtentPercentage = 0.25; ...@@ -17,9 +18,6 @@ const double _kDragContainerExtentPercentage = 0.25;
// displacement; max displacement = _kDragSizeFactorLimit * displacement. // displacement; max displacement = _kDragSizeFactorLimit * displacement.
const double _kDragSizeFactorLimit = 1.5; const double _kDragSizeFactorLimit = 1.5;
// When the scroll ends, the duration of the refresh indicator's animation // When the scroll ends, the duration of the refresh indicator's animation
// to the RefreshIndicator's displacment. // to the RefreshIndicator's displacment.
const Duration _kIndicatorSnapDuration = const Duration(milliseconds: 150); const Duration _kIndicatorSnapDuration = const Duration(milliseconds: 150);
...@@ -33,43 +31,30 @@ const Duration _kIndicatorScaleDuration = const Duration(milliseconds: 200); ...@@ -33,43 +31,30 @@ const Duration _kIndicatorScaleDuration = const Duration(milliseconds: 200);
/// refresh. The returned [Future] must complete when the refresh operation is /// refresh. The returned [Future] must complete when the refresh operation is
/// finished. /// finished.
/// ///
/// Used by [RefreshIndicator.refresh]. /// Used by [RefreshIndicator.onRefresh].
typedef Future<Null> RefreshCallback(); 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 // The state machine moves through these modes only when the scrollable
// identified by scrollableKey has been scrolled to its min or max limit. // identified by scrollableKey has been scrolled to its min or max limit.
enum _RefreshIndicatorMode { enum _RefreshIndicatorMode {
drag, // Pointer is down. drag, // Pointer is down.
armed, // Dragged far enough that an up event will run the refresh callback. armed, // Dragged far enough that an up event will run the onRefresh callback.
snap, // Animating to the indicator's final "displacement". snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback. refresh, // Running the refresh callback.
dismiss, // Animating the indicator's fade-out. done, // Animating the indicator's fade-out after refreshing.
} canceled, // Animating the indicator's fade-out after not arming.
enum _DismissTransition {
shrink, // Refresh callback completed, scale the indicator to 0.
slide, // No refresh, translate the indicator out of view.
} }
/// A widget that supports the Material "swipe to refresh" idiom. /// A widget that supports the Material "swipe to refresh" idiom.
/// ///
/// When the child's vertical Scrollable descendant overscrolls, an /// When the child's [Scrollable2] descendant overscrolls, an animated circular
/// animated circular progress indicator is faded into view. When the scroll /// progress indicator is faded into view. When the scroll ends, if the
/// ends, if the indicator has been dragged far enough for it to become /// indicator has been dragged far enough for it to become completely opaque,
/// completely opaque, the refresh callback is called. The callback is /// the [onRefresh] callback is called. The callback is expected to update the
/// expected to update the scrollable's contents and then complete the Future /// scrollable's contents and then complete the [Future] it returns. The refresh
/// it returns. The refresh indicator disappears after the callback's /// indicator disappears after the callback's [Future] has completed.
/// Future has completed. ///
/// A [RefreshIndicator] can only be used with a vertical scroll view (the xxxxxxx.
/// ///
/// See also: /// See also:
/// ///
...@@ -79,44 +64,33 @@ enum _DismissTransition { ...@@ -79,44 +64,33 @@ enum _DismissTransition {
class RefreshIndicator extends StatefulWidget { class RefreshIndicator extends StatefulWidget {
/// Creates a refresh indicator. /// 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. /// [displacement] is 40.0 logical pixels.
RefreshIndicator({ RefreshIndicator({
Key key, Key key,
this.scrollableKey,
this.child, this.child,
this.displacement: 40.0, this.displacement: 40.0,
this.refresh, this.onRefresh,
this.location: RefreshIndicatorLocation.top,
this.color, this.color,
this.backgroundColor this.backgroundColor
}) : super(key: key) { }) : super(key: key) {
assert(child != null); assert(child != null);
assert(refresh != null); assert(onRefresh != null);
assert(location != 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 /// The refresh indicator will be stacked on top of this child. The indicator
/// will appear when child's Scrollable descendant is over-scrolled. /// will appear when child's Scrollable descendant is over-scrolled.
final Widget child; final Widget child;
/// The distance from the child's top or bottom edge to where the refresh indicator /// The distance from the child's top or bottom edge to where the refresh
/// will settle. During the drag that exposes the refresh indicator, its actual /// indicator will settle. During the drag that exposes the refresh indicator,
/// displacement may significantly exceed this value. /// its actual displacement may significantly exceed this value.
final double displacement; final double displacement;
/// A function that's called when the user has dragged the refresh indicator /// 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 /// far enough to demonstrate that they want the app to refresh. The returned
/// [Future] must complete when the refresh operation is finished. /// [Future] must complete when the refresh operation is finished.
final RefreshCallback refresh; final RefreshCallback onRefresh;
/// Where the refresh indicator should appear, [RefreshIndicatorLocation.top]
/// by default.
final RefreshIndicatorLocation location;
/// The progress indicator's foreground color. The current theme's /// The progress indicator's foreground color. The current theme's
/// [ThemeData.accentColor] by default. /// [ThemeData.accentColor] by default.
...@@ -133,135 +107,180 @@ class RefreshIndicator extends StatefulWidget { ...@@ -133,135 +107,180 @@ class RefreshIndicator extends StatefulWidget {
/// Contains the state for a [RefreshIndicator]. This class can be used to /// Contains the state for a [RefreshIndicator]. This class can be used to
/// programmatically show the refresh indicator, see the [show] method. /// programmatically show the refresh indicator, see the [show] method.
class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderStateMixin { class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderStateMixin {
AnimationController _sizeController; AnimationController _positionController;
AnimationController _scaleController; AnimationController _scaleController;
Animation<double> _sizeFactor; Animation<double> _positionFactor;
Animation<double> _scaleFactor; Animation<double> _scaleFactor;
Animation<double> _value; Animation<double> _value;
Animation<Color> _valueColor; Animation<Color> _valueColor;
double _dragOffset;
bool _isIndicatorAtTop = true;
_RefreshIndicatorMode _mode; _RefreshIndicatorMode _mode;
Future<Null> _pendingRefreshFuture; Future<Null> _pendingRefreshFuture;
bool _isIndicatorAtTop;
double _dragOffset;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_sizeController = new AnimationController(vsync: this); _positionController = new AnimationController(vsync: this);
_sizeFactor = new Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit).animate(_sizeController); _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. _value = new Tween<double>( // The "value" of the circular progress indicator during a drag.
begin: 0.0, begin: 0.0,
end: 0.75 end: 0.75,
).animate(_sizeController); ).animate(_positionController);
_scaleController = new AnimationController(vsync: this); _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 @override
void dispose() { void dispose() {
_sizeController.dispose(); _positionController.dispose();
_scaleController.dispose(); _scaleController.dispose();
super.dispose(); super.dispose();
} }
bool _isValidScrollable(ScrollableState scrollable) { bool _handleScrollNotification(ScrollNotification2 notification) {
if (scrollable == null) 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; return false;
final Axis axis = scrollable.config.scrollDirection; }
return axis == Axis.vertical && scrollable.scrollBehavior is ExtentScrollBehavior; bool indicatorAtTopNow;
} switch (notification.axisDirection) {
case AxisDirection.down:
bool _isScrolledToLimit(ScrollableState scrollable) { indicatorAtTopNow = true;
final double minScrollOffset = scrollable.scrollBehavior.minScrollOffset; break;
final double maxScrollOffset = scrollable.scrollBehavior.maxScrollOffset; case AxisDirection.up:
final double scrollOffset = scrollable.scrollOffset; indicatorAtTopNow = false;
switch (config.location) { break;
case RefreshIndicatorLocation.top: case AxisDirection.left:
return scrollOffset <= minScrollOffset; case AxisDirection.right:
case RefreshIndicatorLocation.bottom: indicatorAtTopNow = null;
return scrollOffset >= maxScrollOffset; 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; return false;
} }
double _overscrollDistance(ScrollableState scrollable) { bool _handleGlowNotification(OverscrollIndicatorNotification notification) {
final double minScrollOffset = scrollable.scrollBehavior.minScrollOffset; if (notification.depth != 0 || !notification.leading)
final double maxScrollOffset = scrollable.scrollBehavior.maxScrollOffset; return false;
final double scrollOffset = scrollable.scrollOffset; if (_mode == _RefreshIndicatorMode.drag) {
switch (config.location) { notification.disallowGlow();
case RefreshIndicatorLocation.top: return true;
return scrollOffset <= minScrollOffset ? -_dragOffset : 0.0;
case RefreshIndicatorLocation.bottom:
return scrollOffset >= maxScrollOffset ? _dragOffset : 0.0;
} }
return 0.0; return false;
} }
void _handlePointerDown(PointerDownEvent event) { bool _start(AxisDirection direction) {
if (_mode != null) assert(_mode == null);
return; assert(_isIndicatorAtTop == null);
assert(_dragOffset == null);
final ScrollableState scrollable = config.scrollableKey.currentState; switch (direction) {
if (!_isValidScrollable(scrollable) || !_isScrolledToLimit(scrollable)) case AxisDirection.down:
return; _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; _dragOffset = 0.0;
_scaleController.value = 0.0; _scaleController.value = 0.0;
_sizeController.value = 0.0; _positionController.value = 0.0;
setState(() { return true;
_mode = _RefreshIndicatorMode.drag;
});
} }
void _handlePointerMove(PointerMoveEvent event) { void _checkDragOffset(double containerExtent) {
if (_mode != _RefreshIndicatorMode.drag && _mode != _RefreshIndicatorMode.armed) assert(_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed);
return; double newValue = _dragOffset / (containerExtent * _kDragContainerExtentPercentage);
if (_mode == _RefreshIndicatorMode.armed)
final ScrollableState scrollable = config.scrollableKey?.currentState; newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
if (!_isValidScrollable(scrollable)) _positionController.value = newValue.clamp(0.0, 1.0); // this triggers various rebuilds
return; if (_mode == _RefreshIndicatorMode.drag && _valueColor.value.alpha == 0xFF)
_mode = _RefreshIndicatorMode.armed;
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;
} }
// Stop showing the refresh indicator // Stop showing the refresh indicator.
Future<Null> _dismiss(_DismissTransition transition) async { Future<Null> _dismiss(_RefreshIndicatorMode newMode) async {
// This can only be called from _show() when refreshing // This can only be called from _show() when refreshing and
// and _handlePointerUp when dragging. // _handleScrollNotification in response to a ScrollEndNotification or
// direction change.
assert(newMode == _RefreshIndicatorMode.canceled || newMode == _RefreshIndicatorMode.done);
setState(() { setState(() {
_mode = _RefreshIndicatorMode.dismiss; _mode = newMode;
}); });
switch(transition) { switch (_mode) {
case _DismissTransition.shrink: case _RefreshIndicatorMode.done:
await _sizeController.animateTo(0.0, duration: _kIndicatorScaleDuration);
break;
case _DismissTransition.slide:
await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration); await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
break; 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(() { setState(() {
_mode = null; _mode = null;
}); });
...@@ -274,125 +293,110 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -274,125 +293,110 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
Completer<Null> completer = new Completer<Null>(); Completer<Null> completer = new Completer<Null>();
_pendingRefreshFuture = completer.future; _pendingRefreshFuture = completer.future;
_mode = _RefreshIndicatorMode.snap; _mode = _RefreshIndicatorMode.snap;
_sizeController _positionController
.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration) .animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration)
.whenComplete(() { .whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.snap) { if (mounted && _mode == _RefreshIndicatorMode.snap) {
assert(config.refresh != null); assert(config.onRefresh != null);
setState(() { setState(() {
// Show the indeterminate progress indicator. // Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh; _mode = _RefreshIndicatorMode.refresh;
}); });
config.refresh().whenComplete(() { config.onRefresh().whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) { if (mounted && _mode == _RefreshIndicatorMode.refresh) {
completer.complete(); 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 /// Show the refresh indicator and run the refresh callback as if it had
/// been started interactively. If this method is called while the refresh /// been started interactively. If this method is called while the refresh
/// callback is running, it quietly does nothing. /// 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]. /// 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 && if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) { _mode != _RefreshIndicatorMode.snap) {
_sizeController.value = 0.0; if (_mode == null)
_scaleController.value = 0.0; _start(atTop ? AxisDirection.down : AxisDirection.up);
_show(); _show();
} }
return _pendingRefreshFuture; return _pendingRefreshFuture;
} }
ScrollableEdge get _clampOverscrollsEdge { final GlobalKey _key = new GlobalKey();
switch (config.location) {
case RefreshIndicatorLocation.top:
return ScrollableEdge.leading;
case RefreshIndicatorLocation.bottom:
return ScrollableEdge.trailing;
}
return ScrollableEdge.none;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); Widget child = new NotificationListener<ScrollNotification2>(
final bool showIndeterminateIndicator = key: _key,
_mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.dismiss; onNotification: _handleScrollNotification,
child: new NotificationListener<OverscrollIndicatorNotification>(
// Fully opaque when we've reached config.displacement. onNotification: _handleGlowNotification,
_valueColor = new ColorTween( child: config.child,
begin: (config.color ?? theme.accentColor).withOpacity(0.0), ),
end: (config.color ?? theme.accentColor).withOpacity(1.0) );
).animate(new CurvedAnimation( if (_mode == null) {
parent: _sizeController, assert(_dragOffset == null);
curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit) assert(_isIndicatorAtTop == null);
)); return child;
}
assert(_dragOffset != null);
assert(_isIndicatorAtTop != null);
return new Listener( final bool showIndeterminateIndicator =
onPointerDown: _handlePointerDown, _mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.done;
onPointerMove: _handlePointerMove,
onPointerUp: _handlePointerUp, return new Stack(
child: new Stack( children: <Widget>[
children: <Widget>[ child,
new ClampOverscrolls.inherit( new Positioned(
context: context, top: _isIndicatorAtTop ? 0.0 : null,
edge: _clampOverscrollsEdge, bottom: !_isIndicatorAtTop ? 0.0 : null,
child: config.child, 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> ...@@ -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) { bool _handleScrollNotification(ScrollNotification2 notification) {
if (notification.depth != 0) if (notification.depth != 0)
return false; return false;
...@@ -119,27 +122,35 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -119,27 +122,35 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
} else { } else {
assert(false); 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(controller != null);
assert(notification.axis == config.axis); assert(notification.axis == config.axis);
if (notification.velocity != 0.0) { if (_accepted[isLeading]) {
assert(notification.dragDetails == null); if (notification.velocity != 0.0) {
controller.absorbImpact(notification.velocity.abs()); assert(notification.dragDetails == null);
} else { controller.absorbImpact(notification.velocity.abs());
assert(notification.overscroll != 0.0); } else {
if (notification.dragDetails != null) { assert(notification.overscroll != 0.0);
assert(notification.dragDetails.globalPosition != null); if (notification.dragDetails != null) {
final RenderBox renderer = notification.context.findRenderObject(); assert(notification.dragDetails.globalPosition != null);
assert(renderer != null); final RenderBox renderer = notification.context.findRenderObject();
assert(renderer.hasSize); assert(renderer != null);
final Size size = renderer.size; assert(renderer.hasSize);
final Point position = renderer.globalToLocal(notification.dragDetails.globalPosition); final Size size = renderer.size;
switch (notification.axis) { final Point position = renderer.globalToLocal(notification.dragDetails.globalPosition);
case Axis.horizontal: switch (notification.axis) {
controller.pull(notification.overscroll.abs(), size.width, position.y.clamp(0.0, size.height), size.height); case Axis.horizontal:
break; controller.pull(notification.overscroll.abs(), size.width, position.y.clamp(0.0, size.height), size.height);
case Axis.vertical: break;
controller.pull(notification.overscroll.abs(), size.height, position.x.clamp(0.0, size.width), size.width); case Axis.vertical:
break; 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> ...@@ -149,6 +160,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
_trailingController.scrollEnd(); _trailingController.scrollEnd();
} }
} }
_lastNotificationType = notification.runtimeType;
return false; return false;
} }
...@@ -466,3 +478,24 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter { ...@@ -466,3 +478,24 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter {
|| oldDelegate.trailingController != trailingController; || 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 { ...@@ -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. /// Creates a notification about scrolling.
ScrollNotification2({ ScrollNotification2({
@required Scrollable2State scrollable, @required Scrollable2State scrollable,
...@@ -75,29 +104,11 @@ abstract class ScrollNotification2 extends LayoutChangedNotification { ...@@ -75,29 +104,11 @@ abstract class ScrollNotification2 extends LayoutChangedNotification {
/// size of the viewport, for instance. /// size of the viewport, for instance.
final BuildContext context; 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 @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('$axisDirection'); description.add('$axisDirection');
description.add('metrics: $metrics'); description.add('metrics: $metrics');
description.add('depth: $depth (${ depth == 0 ? "local" : "remote"})');
} }
} }
......
...@@ -7,8 +7,6 @@ import 'dart:async'; ...@@ -7,8 +7,6 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
bool refreshCalled = false; bool refreshCalled = false;
Future<Null> refresh() { Future<Null> refresh() {
...@@ -26,10 +24,8 @@ void main() { ...@@ -26,10 +24,8 @@ void main() {
refreshCalled = false; refreshCalled = false;
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey, onRefresh: refresh,
refresh: refresh, child: new ListView(
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map((String item) { children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map((String item) {
return new SizedBox( return new SizedBox(
height: 200.0, height: 200.0,
...@@ -52,15 +48,13 @@ void main() { ...@@ -52,15 +48,13 @@ void main() {
refreshCalled = false; refreshCalled = false;
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey, onRefresh: refresh,
refresh: refresh, child: new ListView(
location: RefreshIndicatorLocation.bottom, reverse: true,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
child: new Text('X') child: new Text('X'),
), ),
], ],
), ),
...@@ -75,18 +69,88 @@ void main() { ...@@ -75,18 +69,88 @@ void main() {
expect(refreshCalled, true); 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 { testWidgets('RefreshIndicator - not enough', (WidgetTester tester) async {
refreshCalled = false; refreshCalled = false;
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey, onRefresh: refresh,
refresh: refresh, child: new ListView(
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
child: new Text('X') child: new Text('X'),
), ),
], ],
), ),
...@@ -105,14 +169,12 @@ void main() { ...@@ -105,14 +169,12 @@ void main() {
refreshCalled = false; refreshCalled = false;
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey, onRefresh: holdRefresh, // this one never returns
refresh: holdRefresh, // this one never returns child: new ListView(
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
child: new Text('X') child: new Text('X'),
), ),
], ],
), ),
...@@ -147,14 +209,12 @@ void main() { ...@@ -147,14 +209,12 @@ void main() {
refreshCalled = false; refreshCalled = false;
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey, onRefresh: refresh,
refresh: refresh, child: new ListView(
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
child: new Text('X') child: new Text('X'),
), ),
], ],
), ),
...@@ -190,14 +250,12 @@ void main() { ...@@ -190,14 +250,12 @@ void main() {
refreshCalled = false; refreshCalled = false;
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey, onRefresh: refresh,
refresh: refresh, child: new ListView(
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, 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