Commit 107cbd31 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Refresh Indicator fine tuning (#4800)

parent 6298a1ae
...@@ -381,7 +381,8 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter ...@@ -381,7 +381,8 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter
double tailValue, double tailValue,
int stepValue, int stepValue,
double rotationValue, double rotationValue,
double strokeWidth double strokeWidth,
this.arrowheadScale
}) : super( }) : super(
valueColor: valueColor, valueColor: valueColor,
value: value, value: value,
...@@ -392,23 +393,27 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter ...@@ -392,23 +393,27 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter
strokeWidth: strokeWidth strokeWidth: strokeWidth
); );
final double arrowheadScale;
void paintArrowhead(Canvas canvas, Size size) { void paintArrowhead(Canvas canvas, Size size) {
// ux, uy: a unit vector whose direction parallels the base of the arrowhead. // ux, uy: a unit vector whose direction parallels the base of the arrowhead.
// Note that -ux, uy points in the direction the arrowhead points. // Note that ux, -uy points in the direction the arrowhead points.
final double arcEnd = arcStart + arcSweep; final double arcEnd = arcStart + arcSweep;
final double ux = math.cos(arcEnd); final double ux = math.cos(arcEnd);
final double uy = math.sin(arcEnd); final double uy = math.sin(arcEnd);
assert(size.width == size.height); assert(size.width == size.height);
final double radius = size.width / 2.0; final double radius = size.width / 2.0;
final double arrowHeadRadius = strokeWidth * 1.5; final double arrowheadPointX = radius + ux * radius + -uy * strokeWidth * 2.0 * arrowheadScale;
final double innerRadius = radius - arrowHeadRadius; final double arrowheadPointY = radius + uy * radius + ux * strokeWidth * 2.0 * arrowheadScale;
final double outerRadius = radius + arrowHeadRadius; final double arrowheadRadius = strokeWidth * 1.5 * arrowheadScale;
final double innerRadius = radius - arrowheadRadius;
final double outerRadius = radius + arrowheadRadius;
Path path = new Path() Path path = new Path()
..moveTo(radius + ux * innerRadius, radius + uy * innerRadius) ..moveTo(radius + ux * innerRadius, radius + uy * innerRadius)
..lineTo(radius + ux * outerRadius, radius + uy * outerRadius) ..lineTo(radius + ux * outerRadius, radius + uy * outerRadius)
..lineTo(radius + ux * radius + -uy * strokeWidth * 2.0, radius + uy * radius + ux * strokeWidth * 2.0) ..lineTo(arrowheadPointX, arrowheadPointY)
..close(); ..close();
Paint paint = new Paint() Paint paint = new Paint()
..color = valueColor ..color = valueColor
...@@ -420,7 +425,8 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter ...@@ -420,7 +425,8 @@ class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
super.paint(canvas, size); super.paint(canvas, size);
paintArrowhead(canvas, size); if (arrowheadScale > 0.0)
paintArrowhead(canvas, size);
} }
} }
...@@ -472,6 +478,7 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { ...@@ -472,6 +478,7 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
@override @override
Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) { Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
final double arrowheadScale = config.value == null ? 0.0 : (config.value * 2.0).clamp(0.0, 1.0);
return new Container( return new Container(
width: _kIndicatorSize, width: _kIndicatorSize,
height: _kIndicatorSize, height: _kIndicatorSize,
...@@ -490,7 +497,8 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { ...@@ -490,7 +497,8 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
tailValue: tailValue, tailValue: tailValue,
stepValue: stepValue, stepValue: stepValue,
rotationValue: rotationValue, rotationValue: rotationValue,
strokeWidth: 2.0 strokeWidth: 2.0,
arrowheadScale: arrowheadScale
) )
) )
) )
......
...@@ -37,19 +37,24 @@ typedef Future<Null> RefreshCallback(); ...@@ -37,19 +37,24 @@ typedef Future<Null> RefreshCallback();
/// Where the refresh indicator appears: top for over-scrolls at the /// Where the refresh indicator appears: top for over-scrolls at the
/// start of the scrollable, bottom for over-scrolls at the end. /// start of the scrollable, bottom for over-scrolls at the end.
enum RefreshIndicatorLocation { enum RefreshIndicatorLocation {
/// The refresh indicator should appear at the top of the scrollable. /// The refresh indicator will appear at the top of the scrollable.
top, top,
/// The refresh indicator should appear at the bottom of the scrollable. /// The refresh indicator will appear at the bottom of the scrollable.
bottom, bottom,
/// The refresh indicator will appear at both ends of the scrollable.
both
} }
// 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 { enum _RefreshIndicatorMode {
drag, drag, // Pointer is down.
armed, armed, // Dragged far enough that an up event will run the refresh callback.
snap, snap, // Animating to the indicator's final "displacement".
refresh, refresh, // Running the refresh callback.
dimiss dismiss // Animating the indicator's fade-out.
} }
/// A widget that supports the Material "swipe to refresh" idiom. /// A widget that supports the Material "swipe to refresh" idiom.
...@@ -62,6 +67,11 @@ enum _RefreshIndicatorMode { ...@@ -62,6 +67,11 @@ enum _RefreshIndicatorMode {
/// returns. The refresh indicator disappears after the callback's /// returns. The refresh indicator disappears after the callback's
/// Future has completed. /// Future has completed.
/// ///
/// The required [scrollableKey] parameter identifies the scrollable widget
/// whose scrollOffset is monitored by this RefreshIndicator. The same
/// scrollableKey must also be set on the scrollable. See [Block.scrollableKey]
/// [ScrollableList.scrollableKey], etc.
///
/// See also: /// See also:
/// ///
/// * <https://www.google.com/design/spec/patterns/swipe-to-refresh.html> /// * <https://www.google.com/design/spec/patterns/swipe-to-refresh.html>
...@@ -75,17 +85,22 @@ class RefreshIndicator extends StatefulWidget { ...@@ -75,17 +85,22 @@ class RefreshIndicator extends StatefulWidget {
this.scrollableKey, this.scrollableKey,
this.child, this.child,
this.displacement: 40.0, this.displacement: 40.0,
this.refresh this.refresh,
this.location: RefreshIndicatorLocation.top
}) : super(key: key) { }) : super(key: key) {
assert(child != null); assert(child != null);
assert(refresh != null); assert(refresh != null);
assert(location != null);
} }
/// Identifies the [Scrollable] descendant of child that will cause the /// Identifies the [Scrollable] descendant of child that will cause the
/// refresh indicator to appear. Can be null if there's only one /// refresh indicator to appear.
/// [Scrollable] descendant.
final GlobalKey<ScrollableState> scrollableKey; 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 /// 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 /// will settle. During the drag that exposes the refresh indicator, its actual
/// displacement may significantly exceed this value. /// displacement may significantly exceed this value.
...@@ -96,9 +111,9 @@ class RefreshIndicator extends StatefulWidget { ...@@ -96,9 +111,9 @@ class RefreshIndicator extends StatefulWidget {
/// Future must complete when the refresh operation is finished. /// Future must complete when the refresh operation is finished.
final RefreshCallback refresh; final RefreshCallback refresh;
/// The refresh indicator will be stacked on top of this child. The indicator /// Where the refresh indicator should appear, RefreshIndicatorLocation.top
/// will appear when child's Scrollable descendant is over-scrolled. /// by default.
final Widget child; final RefreshIndicatorLocation location;
@override @override
_RefreshIndicatorState createState() => new _RefreshIndicatorState(); _RefreshIndicatorState createState() => new _RefreshIndicatorState();
...@@ -116,7 +131,7 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -116,7 +131,7 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
double _containerExtent; double _containerExtent;
double _minScrollOffset; double _minScrollOffset;
double _maxScrollOffset; double _maxScrollOffset;
RefreshIndicatorLocation _location = RefreshIndicatorLocation.top; bool _isIndicatorAtTop = true;
_RefreshIndicatorMode _mode; _RefreshIndicatorMode _mode;
Future<Null> _pendingRefreshFuture; Future<Null> _pendingRefreshFuture;
...@@ -165,12 +180,6 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -165,12 +180,6 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
_maxScrollOffset = scrollBehavior.maxScrollOffset; _maxScrollOffset = scrollBehavior.maxScrollOffset;
} }
RefreshIndicatorLocation get _locationForScrollOffset {
return _scrollOffset < _minScrollOffset
? RefreshIndicatorLocation.top
: RefreshIndicatorLocation.bottom;
}
void _handlePointerDown(PointerDownEvent event) { void _handlePointerDown(PointerDownEvent event) {
final ScrollableState scrollable = config.scrollableKey?.currentState; final ScrollableState scrollable = config.scrollableKey?.currentState;
if (scrollable == null) if (scrollable == null)
...@@ -179,27 +188,70 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -179,27 +188,70 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
_updateState(scrollable); _updateState(scrollable);
_scaleController.value = 0.0; _scaleController.value = 0.0;
_sizeController.value = 0.0; _sizeController.value = 0.0;
_mode = _RefreshIndicatorMode.drag; setState(() {
_mode = _RefreshIndicatorMode.drag;
});
} }
void _handlePointerMove(PointerMoveEvent event) { double _overscrollDistance() {
final ScrollableState scrollable = config.scrollableKey?.currentState; final ScrollableState scrollable = config.scrollableKey?.currentState;
if (scrollable == null) if (scrollable == null)
return; return 0.0;
final double value = scrollable.scrollOffset;
if ((value < _minScrollOffset || value > _maxScrollOffset) && final double oldOffset = _scrollOffset;
((value - _scrollOffset).abs() > kPixelScrollTolerance.distance)) { final double newOffset = scrollable.scrollOffset;
final double overScroll = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset; _updateState(scrollable);
final double newValue = overScroll / (_containerExtent * _kDragContainerExtentPercentage);
if ((newOffset - oldOffset).abs() < kPixelScrollTolerance.distance)
return 0.0;
switch (config.location) {
case RefreshIndicatorLocation.top:
return newOffset < _minScrollOffset ? _minScrollOffset - newOffset : 0.0;
case RefreshIndicatorLocation.bottom:
return newOffset > _maxScrollOffset ? newOffset - _maxScrollOffset : 0.0;
case RefreshIndicatorLocation.both: {
if (newOffset < _minScrollOffset)
return _minScrollOffset - newOffset;
else if (newOffset > _maxScrollOffset)
return newOffset - _maxScrollOffset;
else
return 0.0;
}
}
return 0.0;
}
void _handlePointerMove(PointerMoveEvent event) {
final double overscroll = _overscrollDistance();
if (overscroll > 0.0) {
final double newValue = overscroll / (_containerExtent * _kDragContainerExtentPercentage);
_sizeController.value = newValue.clamp(0.0, 1.0); _sizeController.value = newValue.clamp(0.0, 1.0);
if (_location != _locationForScrollOffset) {
final bool newIsAtTop = _scrollOffset < _minScrollOffset;
if (_isIndicatorAtTop != newIsAtTop) {
setState(() { setState(() {
_location = _locationForScrollOffset; _isIndicatorAtTop = newIsAtTop;
}); });
} }
} }
// No setState() here because this doesn't cause a visual change.
_mode = _valueColor.value.alpha == 0xFF ? _RefreshIndicatorMode.armed : _RefreshIndicatorMode.drag; _mode = _valueColor.value.alpha == 0xFF ? _RefreshIndicatorMode.armed : _RefreshIndicatorMode.drag;
_updateState(scrollable); }
// Stop showing the refresh indicator
Future<Null> _dismiss() async {
setState(() {
_mode = _RefreshIndicatorMode.dismiss;
});
await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
if (mounted && _mode == _RefreshIndicatorMode.dismiss) {
setState(() {
_mode = null;
});
}
} }
Future<Null> _doHandlePointerUp(PointerUpEvent event) async { Future<Null> _doHandlePointerUp(PointerUpEvent event) async {
...@@ -220,15 +272,11 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -220,15 +272,11 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
bool completed = _pendingRefreshFuture != null; bool completed = _pendingRefreshFuture != null;
_pendingRefreshFuture = null; _pendingRefreshFuture = null;
if (mounted && completed && _mode == _RefreshIndicatorMode.refresh) { if (mounted && completed && _mode == _RefreshIndicatorMode.refresh)
setState(() { _dismiss();
_mode = null; // Stop showing the indeterminate progress indicator.
});
_scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
}
} }
} else { } else if (_mode == _RefreshIndicatorMode.drag) {
_scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration); _dismiss();
} }
} }
...@@ -238,7 +286,8 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -238,7 +286,8 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isAtTop = _location == RefreshIndicatorLocation.top; final bool showIndeterminateIndicator =
_mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.dismiss;
return new Listener( return new Listener(
onPointerDown: _handlePointerDown, onPointerDown: _handlePointerDown,
onPointerMove: _handlePointerMove, onPointerMove: _handlePointerMove,
...@@ -250,26 +299,28 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -250,26 +299,28 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
value: true value: true
), ),
new Positioned( new Positioned(
top: isAtTop ? 0.0 : null, top: _isIndicatorAtTop ? 0.0 : null,
bottom: isAtTop ? null : 0.0, bottom: _isIndicatorAtTop ? null : 0.0,
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
child: new SizeTransition( child: new SizeTransition(
axisAlignment: isAtTop ? 1.0 : 0.0, axisAlignment: _isIndicatorAtTop ? 1.0 : 0.0,
sizeFactor: _sizeFactor, sizeFactor: _sizeFactor,
child: new Container( child: new Container(
padding: isAtTop padding: _isIndicatorAtTop
? new EdgeInsets.only(top: config.displacement) ? new EdgeInsets.only(top: config.displacement)
: new EdgeInsets.only(bottom: config.displacement), : new EdgeInsets.only(bottom: config.displacement),
child: new Align( child: new Align(
alignment: isAtTop ? FractionalOffset.bottomCenter : FractionalOffset.topCenter, alignment: _isIndicatorAtTop
? FractionalOffset.bottomCenter
: FractionalOffset.topCenter,
child: new ScaleTransition( child: new ScaleTransition(
scale: _scaleFactor, scale: _scaleFactor,
child: new AnimatedBuilder( child: new AnimatedBuilder(
animation: _sizeController, animation: _sizeController,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
return new RefreshProgressIndicator( return new RefreshProgressIndicator(
value: _mode == _RefreshIndicatorMode.refresh ? null : _value.value, value: showIndeterminateIndicator ? null : _value.value,
valueColor: _valueColor valueColor: _valueColor
); );
} }
......
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