Commit a816611d authored by Adam Barth's avatar Adam Barth Committed by GitHub

Port remaining ScrollableViewport tests to SingleChildScrollView (#8129)

Also, rename ScrollableMetrics to ScrollMetrics, which follows the
naming convention for most of the other classes (e.g., ScrollPosition,
ScrollPhysics).

Finally, fix a bug whereby SingleChildScrollView could not have a
GlobalKey, because, write test, find bug.
parent a78d252e
......@@ -98,7 +98,7 @@ class _ScrollbarController extends ChangeNotifier {
super.dispose();
}
ScrollableMetrics _lastMetrics;
ScrollMetrics _lastMetrics;
AxisDirection _lastAxisDirection;
static const double _kMinThumbExtent = 18.0;
......@@ -106,7 +106,7 @@ class _ScrollbarController extends ChangeNotifier {
static const Duration _kThumbFadeDuration = const Duration(milliseconds: 300);
static const Duration _kFadeOutTimeout = const Duration(milliseconds: 600);
void update(ScrollableMetrics metrics, AxisDirection axisDirection) {
void update(ScrollMetrics metrics, AxisDirection axisDirection) {
_lastMetrics = metrics;
_lastAxisDirection = axisDirection;
if (_fadeController.status == AnimationStatus.completed) {
......
......@@ -2,6 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
////////////////////////////////////////////////////////////////////////////////
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
import 'package:flutter/foundation.dart';
import 'framework.dart';
......
......@@ -189,7 +189,7 @@ class PageView extends BoxScrollView {
return new NotificationListener<ScrollNotification2>(
onNotification: (ScrollNotification2 notification) {
if (notification.depth == 1 && onPageChanged != null && notification is ScrollEndNotification) {
final ScrollableMetrics metrics = notification.metrics;
final ScrollMetrics metrics = notification.metrics;
onPageChanged(metrics.extentBefore ~/ metrics.viewportDimension);
}
return false;
......
......@@ -18,11 +18,11 @@ import 'scrollable.dart' show Scrollable2, Scrollable2State;
/// not defined, but must be consistent. For example, they could be in pixels,
/// or in percentages, or in units of the [extentInside] (in the latter case,
/// [extentInside] would always be 1.0).
class ScrollableMetrics {
class ScrollMetrics {
/// Create a description of the metrics of a [Scrollable2]'s contents.
///
/// The three arguments must be present, non-null, finite, and non-negative.
const ScrollableMetrics({
const ScrollMetrics({
@required this.extentBefore,
@required this.extentInside,
@required this.extentAfter,
......@@ -34,9 +34,11 @@ class ScrollableMetrics {
/// described by [extentInside].
final double extentBefore;
/// The quantity of visible content. If [extentBefore] and [extentAfter] are
/// non-zero, then this is typically the height of the viewport. It could be
/// less if there is less content visible than the size of the viewport.
/// The quantity of visible content.
///
/// If [extentBefore] and [extentAfter] are non-zero, then this is typically
/// the height of the viewport. It could be less if there is less content
/// visible than the size of the viewport.
final double extentInside;
/// The quantity of content conceptually "below" the currently visible content
......@@ -65,13 +67,12 @@ abstract class ScrollNotification2 extends LayoutChangedNotification {
Axis get axis => axisDirectionToAxis(axisDirection);
final ScrollableMetrics metrics;
final ScrollMetrics metrics;
/// The build context of the [Scrollable2] that fired this notification.
///
/// This can be used to find the scrollable's render objects to determine the
/// size of the viewport, for instance.
// TODO(ianh): Maybe just fold those into the ScrollableMetrics?
final BuildContext context;
/// The number of [Scrollable2] widgets that this notification has bubbled
......
......@@ -228,8 +228,8 @@ class ScrollPosition extends ViewportOffset {
/// The metrics do not need to be in absolute (pixel) units, but they must be
/// in consistent units (so that they can be compared over time or used to
/// drive diagrammatic user interfaces such as scrollbars).
ScrollableMetrics getMetrics() {
return new ScrollableMetrics(
ScrollMetrics getMetrics() {
return new ScrollMetrics(
extentBefore: math.max(pixels - minScrollExtent, 0.0),
extentInside: math.min(pixels, maxScrollExtent) - math.max(pixels, minScrollExtent) + math.min(viewportDimension, maxScrollExtent - minScrollExtent),
extentAfter: math.max(maxScrollExtent - pixels, 0.0),
......
......@@ -84,7 +84,6 @@ class SingleChildScrollView extends StatelessWidget {
physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new _SingleChildViewport(
key: key,
axisDirection: axisDirection,
offset: offset,
child: contents,
......
......@@ -11,28 +11,26 @@ import 'package:flutter/widgets.dart';
// The top of the bottom widget is at 550 (the top of the top widget
// is at 0). The top of the bottom widget is 500 when it has been
// scrolled completely into view.
Widget buildFrame(ScrollableEdge clampedEdge) {
return new ClampOverscrolls(
edge: clampedEdge,
child: new ScrollableViewport(
key: new UniqueKey(),
child: new SizedBox(
height: 650.0,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new SizedBox(height: 100.0, child: new Text('top')),
new Expanded(child: new Container()),
new SizedBox(height: 100.0, child: new Text('bottom')),
]
)
)
)
Widget buildFrame(ScrollPhysics physics) {
return new SingleChildScrollView(
key: new UniqueKey(),
physics: physics,
child: new SizedBox(
height: 650.0,
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new SizedBox(height: 100.0, child: new Text('top')),
new Expanded(child: new Container()),
new SizedBox(height: 100.0, child: new Text('bottom')),
],
),
),
);
}
void main() {
testWidgets('ClampOverscrolls', (WidgetTester tester) async {
testWidgets('ClampingScrollPhysics', (WidgetTester tester) async {
// Scroll the target text widget by offset and then return its origin
// in global coordinates.
......@@ -45,105 +43,49 @@ void main() {
return new Future<Point>.value(widgetOrigin);
}
// Each of the blocks below test overscrolling the top and bottom
// with a value for ClampOverscrolls.edge.
await tester.pumpWidget(buildFrame(ScrollableEdge.none));
await tester.pumpWidget(buildFrame(const BouncingScrollPhysics()));
Point origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, greaterThan(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, lessThan(500.0));
await tester.pumpWidget(buildFrame(ScrollableEdge.both));
await tester.pumpWidget(buildFrame(const ClampingScrollPhysics()));
origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, equals(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, equals(500.0));
await tester.pumpWidget(buildFrame(ScrollableEdge.leading));
origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, equals(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, lessThan(500.0));
await tester.pumpWidget(buildFrame(ScrollableEdge.trailing));
origin = await locationAfterScroll('top', const Offset(0.0, 400.0));
expect(origin.y, greaterThan(0.0));
origin = await locationAfterScroll('bottom', const Offset(0.0, -400.0));
expect(origin.y, equals(500.0));
});
testWidgets('ClampOverscrolls affects scrollOffset not virtualScrollOffset', (WidgetTester tester) async {
testWidgets('ClampingScrollPhysics affects ScrollPosition', (WidgetTester tester) async {
// ClampOverscrolls.edge == ScrollableEdge.none
// BouncingScrollPhysics
await tester.pumpWidget(buildFrame(ScrollableEdge.none));
StatefulElement statefulElement = tester.element(find.byType(Scrollable));
ScrollableState scrollable = statefulElement.state;
await tester.pumpWidget(buildFrame(const BouncingScrollPhysics()));
Scrollable2State scrollable = tester.state(find.byType(Scrollable2));
await tester.scrollAt(tester.getTopLeft(find.text('top')), const Offset(0.0, 400.0));
await tester.pump();
expect(scrollable.scrollOffset, lessThan(0.0));
expect(scrollable.virtualScrollOffset, equals(scrollable.scrollOffset));
expect(scrollable.position.pixels, lessThan(0.0));
await tester.pump(const Duration(seconds: 1)); // Allow overscroll to settle
await tester.scrollAt(tester.getTopLeft(find.text('bottom')), const Offset(0.0, -400.0));
await tester.pump();
expect(scrollable.scrollOffset, greaterThan(0.0));
expect(scrollable.virtualScrollOffset, equals(scrollable.scrollOffset));
expect(scrollable.position.pixels, greaterThan(0.0));
await tester.pump(const Duration(seconds: 1)); // Allow overscroll to settle
// ClampOverscrolls.edge == ScrollableEdge.both
await tester.pumpWidget(buildFrame(ScrollableEdge.both));
statefulElement = tester.element(find.byType(Scrollable));
scrollable = statefulElement.state;
await tester.scrollAt(tester.getTopLeft(find.text('top')), const Offset(0.0, 400.0));
await tester.pump();
expect(scrollable.scrollOffset, equals(0.0));
expect(scrollable.virtualScrollOffset, lessThan(0.0));
await tester.pump(const Duration(seconds: 1)); // Allow overscroll to settle
await tester.scrollAt(tester.getTopLeft(find.text('bottom')), const Offset(0.0, -400.0));
await tester.pump();
expect(scrollable.scrollOffset, equals(50.0));
expect(scrollable.virtualScrollOffset, greaterThan(50.0));
// ClampOverscrolls.edge == ScrollableEdge.leading
await tester.pumpWidget(buildFrame(ScrollableEdge.leading));
statefulElement = tester.element(find.byType(Scrollable));
scrollable = statefulElement.state;
await tester.scrollAt(tester.getTopLeft(find.text('top')), const Offset(0.0, 400.0));
await tester.pump();
expect(scrollable.scrollOffset, equals(0.0));
expect(scrollable.virtualScrollOffset, lessThan(0.0));
await tester.pump(const Duration(seconds: 1)); // Allow overscroll to settle
await tester.scrollAt(tester.getTopLeft(find.text('bottom')), const Offset(0.0, -400.0));
await tester.pump();
expect(scrollable.scrollOffset, greaterThan(0.0));
expect(scrollable.virtualScrollOffset, equals(scrollable.scrollOffset));
// ClampOverscrolls.edge == ScrollableEdge.trailing
// ClampingScrollPhysics
await tester.pumpWidget(buildFrame(ScrollableEdge.trailing));
statefulElement = tester.element(find.byType(Scrollable));
scrollable = statefulElement.state;
await tester.pumpWidget(buildFrame(const ClampingScrollPhysics()));
scrollable = scrollable = tester.state(find.byType(Scrollable2));
await tester.scrollAt(tester.getTopLeft(find.text('top')), const Offset(0.0, 400.0));
await tester.pump();
expect(scrollable.scrollOffset, lessThan(0.0));
expect(scrollable.virtualScrollOffset, equals(scrollable.scrollOffset));
expect(scrollable.virtualScrollOffset, equals(scrollable.scrollOffset));
expect(scrollable.position.pixels, equals(0.0));
await tester.pump(const Duration(seconds: 1)); // Allow overscroll to settle
await tester.scrollAt(tester.getTopLeft(find.text('bottom')), const Offset(0.0, -400.0));
await tester.pump();
expect(scrollable.scrollOffset, equals(50.0));
expect(scrollable.virtualScrollOffset, greaterThan(50.0));
expect(scrollable.position.pixels, equals(50.0));
});
}
......@@ -2,110 +2,70 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class TestScrollConfigurationDelegate extends ScrollConfigurationDelegate {
TestScrollConfigurationDelegate(this.flag);
class TestScrollBehavior extends ScrollBehavior2 {
TestScrollBehavior(this.flag);
final bool flag;
@override
TargetPlatform get platform => defaultTargetPlatform;
@override
ExtentScrollBehavior createScrollBehavior() {
ScrollPhysics getScrollPhysics(BuildContext context) {
return flag
? new BoundedBehavior(platform: platform)
: new UnboundedBehavior(platform: platform);
? const ClampingScrollPhysics()
: const BouncingScrollPhysics();
}
@override
bool updateShouldNotify(TestScrollConfigurationDelegate old) => flag != old.flag;
bool shouldNotify(TestScrollBehavior old) => flag != old.flag;
}
void main() {
test('BoundedBehavior min scroll offset', () {
BoundedBehavior behavior = new BoundedBehavior(
contentExtent: 150.0,
containerExtent: 75.0,
minScrollOffset: -100.0,
platform: TargetPlatform.iOS
);
expect(behavior.minScrollOffset, equals(-100.0));
expect(behavior.maxScrollOffset, equals(-25.0));
double scrollOffset = behavior.updateExtents(
contentExtent: 125.0,
containerExtent: 50.0,
scrollOffset: -80.0
);
expect(behavior.minScrollOffset, equals(-100.0));
expect(behavior.maxScrollOffset, equals(-25.0));
expect(scrollOffset, equals(-80.0));
scrollOffset = behavior.updateExtents(
minScrollOffset: 50.0,
scrollOffset: scrollOffset
);
expect(behavior.minScrollOffset, equals(50.0));
expect(behavior.maxScrollOffset, equals(125.0));
expect(scrollOffset, equals(50.0));
});
testWidgets('Inherited ScrollConfiguration changed', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey(debugLabel: 'scrollable');
TestScrollConfigurationDelegate delegate;
ExtentScrollBehavior behavior;
TestScrollBehavior behavior;
ScrollPosition position;
Widget scrollView = new SingleChildScrollView(
key: key,
child: new Builder(
builder: (BuildContext context) {
behavior = ScrollConfiguration2.of(context);
position = Scrollable2.of(context).position;
return new Container(height: 1000.0);
},
),
);
await tester.pumpWidget(
new ScrollConfiguration(
delegate: new TestScrollConfigurationDelegate(true),
child: new ScrollableViewport(
key: key,
child: new Builder(
builder: (BuildContext context) {
delegate = ScrollConfiguration.of(context);
behavior = Scrollable.of(context).scrollBehavior;
return new Container(height: 1000.0);
}
)
)
)
new ScrollConfiguration2(
behavior: new TestScrollBehavior(true),
child: scrollView,
),
);
expect(delegate, isNotNull);
expect(delegate.flag, isTrue);
expect(behavior, const isInstanceOf<BoundedBehavior>());
expect(behavior.contentExtent, equals(1000.0));
expect(behavior.containerExtent, equals(600.0));
expect(behavior, isNotNull);
expect(behavior.flag, isTrue);
expect(position.physics, const isInstanceOf<ClampingScrollPhysics>());
ScrollMetrics metrics = position.getMetrics();
expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.0));
// Same Scrollable, different ScrollConfiguration
await tester.pumpWidget(
new ScrollConfiguration(
delegate: new TestScrollConfigurationDelegate(false),
child: new ScrollableViewport(
key: key,
child: new Builder(
builder: (BuildContext context) {
delegate = ScrollConfiguration.of(context);
behavior = Scrollable.of(context).scrollBehavior;
return new Container(height: 1000.0);
}
)
)
)
new ScrollConfiguration2(
behavior: new TestScrollBehavior(false),
child: scrollView,
),
);
expect(delegate, isNotNull);
expect(delegate.flag, isFalse);
expect(behavior, const isInstanceOf<UnboundedBehavior>());
expect(behavior, isNotNull);
expect(behavior.flag, isFalse);
expect(position.physics, const isInstanceOf<BouncingScrollPhysics>());
// Regression test for https://github.com/flutter/flutter/issues/5856
expect(behavior.contentExtent, equals(1000.0));
expect(behavior.containerExtent, equals(600.0));
metrics = position.getMetrics();
expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.0));
});
}
......@@ -41,19 +41,19 @@ class TestScrollPosition extends ScrollPosition {
}
@override
ScrollableMetrics getMetrics() {
ScrollMetrics getMetrics() {
double insideExtent = viewportDimension;
double beforeExtent = _pixels - minScrollExtent;
double afterExtent = maxScrollExtent - _pixels;
if (insideExtent > 0.0) {
return new ScrollableMetrics(
return new ScrollMetrics(
extentBefore: physics.extentMultiplier * beforeExtent / insideExtent,
extentInside: physics.extentMultiplier,
extentAfter: physics.extentMultiplier * afterExtent / insideExtent,
viewportDimension: viewportDimension,
);
} else {
return new ScrollableMetrics(
return new ScrollMetrics(
extentBefore: 0.0,
extentInside: 0.0,
extentAfter: 0.0,
......
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