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