Refresh Indicator fine tuning (#4800)
This commit is contained in:
parent
6298a1aeb1
commit
107cbd3185
@ -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
|
|||||||
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
|
|||||||
@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 {
|
|||||||
|
|
||||||
@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 {
|
|||||||
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();
|
|||||||
/// 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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
/// 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> {
|
|||||||
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> {
|
|||||||
_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> {
|
|||||||
_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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
double _overscrollDistance() {
|
||||||
|
final ScrollableState scrollable = config.scrollableKey?.currentState;
|
||||||
|
if (scrollable == null)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
final double oldOffset = _scrollOffset;
|
||||||
|
final double newOffset = scrollable.scrollOffset;
|
||||||
|
_updateState(scrollable);
|
||||||
|
|
||||||
|
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) {
|
void _handlePointerMove(PointerMoveEvent event) {
|
||||||
final ScrollableState scrollable = config.scrollableKey?.currentState;
|
final double overscroll = _overscrollDistance();
|
||||||
if (scrollable == null)
|
if (overscroll > 0.0) {
|
||||||
return;
|
final double newValue = overscroll / (_containerExtent * _kDragContainerExtentPercentage);
|
||||||
final double value = scrollable.scrollOffset;
|
|
||||||
if ((value < _minScrollOffset || value > _maxScrollOffset) &&
|
|
||||||
((value - _scrollOffset).abs() > kPixelScrollTolerance.distance)) {
|
|
||||||
final double overScroll = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset;
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
@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> {
|
|||||||
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user