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 {
class OverscrollDemoState extends State<OverscrollDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = new GlobalKey<RefreshIndicatorState>();
static final GlobalKey<ScrollableState> _scrollableKey = new GlobalKey<ScrollableState>();
static final List<String> _items = <String>[
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N'
];
IndicatorType _type = IndicatorType.refresh;
Future<Null> refresh() {
Future<Null> _handleRefresh() {
Completer<Null> completer = new Completer<Null>();
new Timer(new Duration(seconds: 3), () { completer.complete(null); });
return completer.future.then((_) {
......@@ -45,62 +42,37 @@ class OverscrollDemoState extends State<OverscrollDemo> {
@override
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(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('$indicatorTypeText'),
title: new Text('Pull to refresh'),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.refresh),
tooltip: 'Pull to refresh',
tooltip: 'Refresh',
onPressed: () {
setState(() {
_type = IndicatorType.refresh;
});
_refreshIndicatorKey.currentState.show();
}
),
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>
}
}
Type _lastNotificationType;
final Map<bool, bool> _accepted = <bool, bool>{false: true, true: true};
bool _handleScrollNotification(ScrollNotification2 notification) {
if (notification.depth != 0)
return false;
......@@ -119,27 +122,35 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
} else {
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(notification.axis == config.axis);
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
controller.absorbImpact(notification.velocity.abs());
} else {
assert(notification.overscroll != 0.0);
if (notification.dragDetails != null) {
assert(notification.dragDetails.globalPosition != null);
final RenderBox renderer = notification.context.findRenderObject();
assert(renderer != null);
assert(renderer.hasSize);
final Size size = renderer.size;
final Point position = renderer.globalToLocal(notification.dragDetails.globalPosition);
switch (notification.axis) {
case Axis.horizontal:
controller.pull(notification.overscroll.abs(), size.width, position.y.clamp(0.0, size.height), size.height);
break;
case Axis.vertical:
controller.pull(notification.overscroll.abs(), size.height, position.x.clamp(0.0, size.width), size.width);
break;
if (_accepted[isLeading]) {
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
controller.absorbImpact(notification.velocity.abs());
} else {
assert(notification.overscroll != 0.0);
if (notification.dragDetails != null) {
assert(notification.dragDetails.globalPosition != null);
final RenderBox renderer = notification.context.findRenderObject();
assert(renderer != null);
assert(renderer.hasSize);
final Size size = renderer.size;
final Point position = renderer.globalToLocal(notification.dragDetails.globalPosition);
switch (notification.axis) {
case Axis.horizontal:
controller.pull(notification.overscroll.abs(), size.width, position.y.clamp(0.0, size.height), size.height);
break;
case Axis.vertical:
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>
_trailingController.scrollEnd();
}
}
_lastNotificationType = notification.runtimeType;
return false;
}
......@@ -466,3 +478,24 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter {
|| 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 {
}
}
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.
ScrollNotification2({
@required Scrollable2State scrollable,
......@@ -75,29 +104,11 @@ abstract class ScrollNotification2 extends LayoutChangedNotification {
/// size of the viewport, for instance.
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
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
description.add('metrics: $metrics');
description.add('depth: $depth (${ depth == 0 ? "local" : "remote"})');
}
}
......
......@@ -7,8 +7,6 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>();
bool refreshCalled = false;
Future<Null> refresh() {
......@@ -26,10 +24,8 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map((String item) {
return new SizedBox(
height: 200.0,
......@@ -52,15 +48,13 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
location: RefreshIndicatorLocation.bottom,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
reverse: true,
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -75,18 +69,88 @@ void main() {
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 {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -105,14 +169,12 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: holdRefresh, // this one never returns
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: holdRefresh, // this one never returns
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -147,14 +209,12 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <Widget>[
new SizedBox(
height: 200.0,
child: new Text('X')
child: new Text('X'),
),
],
),
......@@ -190,14 +250,12 @@ void main() {
refreshCalled = false;
await tester.pumpWidget(
new RefreshIndicator(
scrollableKey: scrollableKey,
refresh: refresh,
child: new Block( // ignore: DEPRECATED_MEMBER_USE
scrollableKey: scrollableKey,
onRefresh: refresh,
child: new ListView(
children: <Widget>[
new SizedBox(
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