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.'),
);
},
),
),
); );
} }
} }
...@@ -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,8 +122,15 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -119,8 +122,15 @@ 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 (_accepted[isLeading]) {
if (notification.velocity != 0.0) { if (notification.velocity != 0.0) {
assert(notification.dragDetails == null); assert(notification.dragDetails == null);
controller.absorbImpact(notification.velocity.abs()); controller.absorbImpact(notification.velocity.abs());
...@@ -143,12 +153,14 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -143,12 +153,14 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
} }
} }
} }
}
} else if (notification is ScrollEndNotification || notification is ScrollUpdateNotification) { } else if (notification is ScrollEndNotification || notification is ScrollUpdateNotification) {
if (notification.dragDetails != null) { // ignore: undefined_getter if (notification.dragDetails != null) { // ignore: undefined_getter
_leadingController.scrollEnd(); _leadingController.scrollEnd();
_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