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

Refresh indicator (#4788)

parent 63eedb76
...@@ -18,6 +18,7 @@ class OverscrollDemo extends StatefulWidget { ...@@ -18,6 +18,7 @@ class OverscrollDemo extends StatefulWidget {
} }
class OverscrollDemoState extends State<OverscrollDemo> { class OverscrollDemoState extends State<OverscrollDemo> {
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'
]; ];
...@@ -50,6 +51,7 @@ class OverscrollDemoState extends State<OverscrollDemo> { ...@@ -50,6 +51,7 @@ class OverscrollDemoState extends State<OverscrollDemo> {
child: new MaterialList( child: new MaterialList(
type: MaterialListType.threeLine, type: MaterialListType.threeLine,
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
scrollableKey: _scrollableKey,
children: _items.map((String item) { children: _items.map((String item) {
return new ListItem( return new ListItem(
isThreeLine: true, isThreeLine: true,
...@@ -65,7 +67,11 @@ class OverscrollDemoState extends State<OverscrollDemo> { ...@@ -65,7 +67,11 @@ class OverscrollDemoState extends State<OverscrollDemo> {
body = new OverscrollIndicator(child: body); body = new OverscrollIndicator(child: body);
break; break;
case IndicatorType.refresh: case IndicatorType.refresh:
body = new RefreshIndicator(child: body, refresh: refresh); body = new RefreshIndicator(
child: body,
refresh: refresh,
scrollableKey: _scrollableKey
);
break; break;
} }
......
...@@ -60,6 +60,8 @@ class SawTooth extends Curve { ...@@ -60,6 +60,8 @@ class SawTooth extends Curve {
@override @override
double transform(double t) { double transform(double t) {
if (t == 1.0)
return 1.0;
t *= count; t *= count;
return t - t.truncateToDouble(); return t - t.truncateToDouble();
} }
......
...@@ -96,7 +96,7 @@ class MaterialList extends StatelessWidget { ...@@ -96,7 +96,7 @@ class MaterialList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new ScrollableList( return new ScrollableList(
key: scrollableKey, scrollableKey: scrollableKey,
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
onScrollStart: onScrollStart, onScrollStart: onScrollStart,
......
...@@ -317,9 +317,7 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> { ...@@ -317,9 +317,7 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = new AnimationController( _controller = _buildController();
duration: const Duration(milliseconds: 6666)
)..repeat();
} }
@override @override
...@@ -328,6 +326,10 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> { ...@@ -328,6 +326,10 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
super.dispose(); super.dispose();
} }
AnimationController _buildController() {
return new AnimationController(duration: const Duration(milliseconds: 6666))..repeat();
}
Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) { Widget _buildIndicator(BuildContext context, double headValue, double tailValue, int stepValue, double rotationValue) {
return new Container( return new Container(
constraints: new BoxConstraints( constraints: new BoxConstraints(
...@@ -348,11 +350,7 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> { ...@@ -348,11 +350,7 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
); );
} }
@override Widget _buildAnimation() {
Widget build(BuildContext context) {
if (config.value != null)
return _buildIndicator(context, 0.0, 0.0, 0, 0.0);
return new AnimatedBuilder( return new AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
...@@ -366,6 +364,13 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> { ...@@ -366,6 +364,13 @@ class _CircularProgressIndicatorState extends State<CircularProgressIndicator> {
} }
); );
} }
@override
Widget build(BuildContext context) {
if (config.value != null)
return _buildIndicator(context, 0.0, 0.0, 0, 0.0);
return _buildAnimation();
}
} }
class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter { class _RefreshProgressIndicatorPainter extends _CircularProgressIndicatorPainter {
...@@ -438,7 +443,12 @@ class RefreshProgressIndicator extends CircularProgressIndicator { ...@@ -438,7 +443,12 @@ class RefreshProgressIndicator extends CircularProgressIndicator {
double value, double value,
Color backgroundColor, Color backgroundColor,
Animation<Color> valueColor Animation<Color> valueColor
}) : super(key: key, value: value, backgroundColor: backgroundColor, valueColor: valueColor); }) : super(
key: key,
value: value,
backgroundColor: backgroundColor,
valueColor: valueColor
);
@override @override
_RefreshProgressIndicatorState createState() => new _RefreshProgressIndicatorState(); _RefreshProgressIndicatorState createState() => new _RefreshProgressIndicatorState();
...@@ -447,6 +457,19 @@ class RefreshProgressIndicator extends CircularProgressIndicator { ...@@ -447,6 +457,19 @@ class RefreshProgressIndicator extends CircularProgressIndicator {
class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
static double _kIndicatorSize = 40.0; static double _kIndicatorSize = 40.0;
// Always show the indeterminate version of the circular progress indicator.
// When value is non-null the sweep of the progress indicator arrow's arc
// varies from 0 to about 270 degrees. When value is null the arrow animates
// starting from wherever we left it.
@override
Widget build(BuildContext context) {
if (config.value != null)
_controller.value = config.value / 10.0;
else
_controller.forward();
return _buildAnimation();
}
@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) {
return new Container( return new Container(
...@@ -462,8 +485,8 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState { ...@@ -462,8 +485,8 @@ class _RefreshProgressIndicatorState extends _CircularProgressIndicatorState {
child: new CustomPaint( child: new CustomPaint(
painter: new _RefreshProgressIndicatorPainter( painter: new _RefreshProgressIndicatorPainter(
valueColor: config._getValueColor(context), valueColor: config._getValueColor(context),
value: config.value, // may be null value: null, // Draw the indeterminate progress indicator.
headValue: headValue, // remaining arguments are ignored if config.value is not null headValue: headValue,
tailValue: tailValue, tailValue: tailValue,
stepValue: stepValue, stepValue: stepValue,
rotationValue: rotationValue, rotationValue: rotationValue,
......
...@@ -44,6 +44,14 @@ enum RefreshIndicatorLocation { ...@@ -44,6 +44,14 @@ enum RefreshIndicatorLocation {
bottom, bottom,
} }
enum _RefreshIndicatorMode {
drag,
armed,
snap,
refresh,
dimiss
}
/// 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 vertical Scrollable descendant overscrolls, an
...@@ -76,7 +84,7 @@ class RefreshIndicator extends StatefulWidget { ...@@ -76,7 +84,7 @@ class RefreshIndicator extends StatefulWidget {
/// 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. Can be null if there's only one
/// [Scrollable] descendant. /// [Scrollable] descendant.
final Key scrollableKey; final GlobalKey<ScrollableState> scrollableKey;
/// 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
...@@ -101,6 +109,7 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -101,6 +109,7 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
final AnimationController _scaleController = new AnimationController(); final AnimationController _scaleController = new AnimationController();
Animation<double> _sizeFactor; Animation<double> _sizeFactor;
Animation<double> _scaleFactor; Animation<double> _scaleFactor;
Animation<double> _value;
Animation<Color> _valueColor; Animation<Color> _valueColor;
double _scrollOffset; double _scrollOffset;
...@@ -108,6 +117,8 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -108,6 +117,8 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
double _minScrollOffset; double _minScrollOffset;
double _maxScrollOffset; double _maxScrollOffset;
RefreshIndicatorLocation _location = RefreshIndicatorLocation.top; RefreshIndicatorLocation _location = RefreshIndicatorLocation.top;
_RefreshIndicatorMode _mode;
Future<Null> _pendingRefreshFuture;
@override @override
void initState() { void initState() {
...@@ -117,6 +128,13 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -117,6 +128,13 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
// The "value" of the circular progress indicator during a drag.
_value = new Tween<double>(
begin: 0.0,
end: 0.75
)
.animate(_sizeController);
// Fully opaque when we've reached config.displacement. // Fully opaque when we've reached config.displacement.
_valueColor = new ColorTween( _valueColor = new ColorTween(
begin: theme.primaryColor.withOpacity(0.0), begin: theme.primaryColor.withOpacity(0.0),
...@@ -126,6 +144,7 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -126,6 +144,7 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
parent: _sizeController, parent: _sizeController,
curve: new Interval(0.0, 1.0 / _kDragSizeFactorLimit) curve: new Interval(0.0, 1.0 / _kDragSizeFactorLimit)
)); ));
} }
@override @override
...@@ -146,73 +165,84 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -146,73 +165,84 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
_maxScrollOffset = scrollBehavior.maxScrollOffset; _maxScrollOffset = scrollBehavior.maxScrollOffset;
} }
void _onScrollStarted(ScrollableState scrollable) {
_updateState(scrollable);
_scaleController.value = 0.0;
_sizeController.value = 0.0;
}
RefreshIndicatorLocation get _locationForScrollOffset { RefreshIndicatorLocation get _locationForScrollOffset {
return _scrollOffset < _minScrollOffset return _scrollOffset < _minScrollOffset
? RefreshIndicatorLocation.top ? RefreshIndicatorLocation.top
: RefreshIndicatorLocation.bottom; : RefreshIndicatorLocation.bottom;
} }
void _onScrollUpdated(ScrollableState scrollable) { void _handlePointerDown(PointerDownEvent event) {
final ScrollableState scrollable = config.scrollableKey?.currentState;
if (scrollable == null)
return;
_updateState(scrollable);
_scaleController.value = 0.0;
_sizeController.value = 0.0;
_mode = _RefreshIndicatorMode.drag;
}
void _handlePointerMove(PointerMoveEvent event) {
final ScrollableState scrollable = config.scrollableKey?.currentState;
if (scrollable == null)
return;
final double value = scrollable.scrollOffset; final double value = scrollable.scrollOffset;
if ((value < _minScrollOffset || value > _maxScrollOffset) && if ((value < _minScrollOffset || value > _maxScrollOffset) &&
((value - _scrollOffset).abs() > kPixelScrollTolerance.distance)) { ((value - _scrollOffset).abs() > kPixelScrollTolerance.distance)) {
final double overScroll = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset; final double overScroll = value < _minScrollOffset ? _minScrollOffset - value : value - _maxScrollOffset;
final double newValue = overScroll / (_containerExtent * _kDragContainerExtentPercentage); final double newValue = overScroll / (_containerExtent * _kDragContainerExtentPercentage);
if (newValue > _sizeController.value) { _sizeController.value = newValue.clamp(0.0, 1.0);
_sizeController.value = newValue; if (_location != _locationForScrollOffset) {
if (_location != _locationForScrollOffset) { setState(() {
setState(() { _location = _locationForScrollOffset;
_location = _locationForScrollOffset; });
});
}
} }
} }
_mode = _valueColor.value.alpha == 0xFF ? _RefreshIndicatorMode.armed : _RefreshIndicatorMode.drag;
_updateState(scrollable); _updateState(scrollable);
} }
Future<Null> _doOnScrollEnded(ScrollableState scrollable) async { Future<Null> _doHandlePointerUp(PointerUpEvent event) async {
if (_valueColor.value.alpha == 0xFF) { if (_mode == _RefreshIndicatorMode.armed) {
_mode = _RefreshIndicatorMode.snap;
await _sizeController.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration); await _sizeController.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration);
await config.refresh(); if (mounted && _mode == _RefreshIndicatorMode.snap) {
} setState(() {
return _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration); _mode = _RefreshIndicatorMode.refresh; // Show the indeterminate progress indicator.
} });
void _onScrollEnded(ScrollableState scrollable) { // Only one refresh callback is allowed to run at a time. If the user
_doOnScrollEnded(scrollable); // attempts to start a refresh while one is still running ("pending") we
} // just continue to wait on the pending refresh.
if (_pendingRefreshFuture == null)
_pendingRefreshFuture = config.refresh();
await _pendingRefreshFuture;
bool completed = _pendingRefreshFuture != null;
_pendingRefreshFuture = null;
bool _handleScrollNotification(ScrollNotification notification) { if (mounted && completed && _mode == _RefreshIndicatorMode.refresh) {
if (config.scrollableKey == null || config.scrollableKey == notification.scrollable.config.key) { setState(() {
final ScrollableState scrollable = notification.scrollable; _mode = null; // Stop showing the indeterminate progress indicator.
if (scrollable.config.scrollDirection != Axis.vertical) });
return false; _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
switch(notification.kind) { }
case ScrollNotificationKind.started:
_onScrollStarted(scrollable);
break;
case ScrollNotificationKind.updated:
_onScrollUpdated(scrollable);
break;
case ScrollNotificationKind.ended:
_onScrollEnded(scrollable);
break;
} }
} else {
_scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
} }
return false; }
void _handlePointerUp(PointerEvent event) {
_doHandlePointerUp(event);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool isAtTop = _location == RefreshIndicatorLocation.top; final bool isAtTop = _location == RefreshIndicatorLocation.top;
return new NotificationListener<ScrollNotification>( return new Listener(
onNotification: _handleScrollNotification, onPointerDown: _handlePointerDown,
onPointerMove: _handlePointerMove,
onPointerUp: _handlePointerUp,
child: new Stack( child: new Stack(
children: <Widget>[ children: <Widget>[
new ClampOverscrolls( new ClampOverscrolls(
...@@ -235,9 +265,14 @@ class _RefreshIndicatorState extends State<RefreshIndicator> { ...@@ -235,9 +265,14 @@ class _RefreshIndicatorState extends State<RefreshIndicator> {
alignment: isAtTop ? FractionalOffset.bottomCenter : FractionalOffset.topCenter, alignment: isAtTop ? FractionalOffset.bottomCenter : FractionalOffset.topCenter,
child: new ScaleTransition( child: new ScaleTransition(
scale: _scaleFactor, scale: _scaleFactor,
child: new RefreshProgressIndicator( child: new AnimatedBuilder(
value: null, animation: _sizeController,
valueColor: _valueColor builder: (BuildContext context, Widget child) {
return new RefreshProgressIndicator(
value: _mode == _RefreshIndicatorMode.refresh ? null : _value.value,
valueColor: _valueColor
);
}
) )
) )
) )
......
...@@ -7,8 +7,9 @@ import 'dart:async'; ...@@ -7,8 +7,9 @@ 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';
void main() { final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
void main() {
bool refreshCalled = false; bool refreshCalled = false;
Future<Null> refresh() { Future<Null> refresh() {
...@@ -19,8 +20,10 @@ void main() { ...@@ -19,8 +20,10 @@ void main() {
testWidgets('RefreshIndicator', (WidgetTester tester) async { testWidgets('RefreshIndicator', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new RefreshIndicator( new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh, refresh: refresh,
child: new Block( child: new Block(
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,
...@@ -31,7 +34,7 @@ void main() { ...@@ -31,7 +34,7 @@ void main() {
) )
); );
await tester.fling(find.text('A'), const Offset(0.0, 200.0), -1000.0); await tester.fling(find.text('A'), const Offset(0.0, 300.0), -1000.0);
await tester.pump(); await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
......
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