Improve our scroll physics on iOS (#5340)
Changes in this patch:
- iOS now uses a different scrollDrag constant than Android.
- ScrollConfigurationDelegate now knows about target platforms.
- ScrollBehaviors now know about target platforms.
- RawInputLine now has to be told what platform it's targetting.
- PageableList now has a concept of target platform.
- make debugPrintStack filter its stack.
- move debugPrintStack to `assertions.dart`.
- add support for limiting the number of frames to debugPrintStack.
- make defaultTargetPlatform default to android in test environments.
- remove OverscrollStyle and MaterialApp's overscrollStyle argument. You
can now control the overscroll style using Theme.platform.
- the default scroll configuration is now private to avoid people
relying on the defaultTargetPlatform getter in their subclasses (since
they really should use Theme.of(context).platform).
- fix some typos I noticed in some tests.
- added a test for flinging scrollables, that checks that the behavior
differs on the two target platforms.
- made flingFrom and fling in the test API pump the frames.
- added more docs to the test API.
- made the TestAsyncUtils.guard() method report uncaught errors to help
debug errors when using that API.
diff --git a/packages/flutter/lib/src/foundation/assertions.dart b/packages/flutter/lib/src/foundation/assertions.dart
index 4457dbd..f87665c 100644
--- a/packages/flutter/lib/src/foundation/assertions.dart
+++ b/packages/flutter/lib/src/foundation/assertions.dart
@@ -326,3 +326,17 @@
onError(details);
}
}
+
+/// Dump the current stack to the console using [debugPrint] and
+/// [FlutterError.defaultStackFilter].
+///
+/// The current stack is obtained using [StackTrace.current].
+///
+/// The `maxFrames` argument can be given to limit the stack to the given number
+/// of lines. By default, all non-filtered stack lines are shown.
+void debugPrintStack({ int maxFrames }) {
+ List<String> lines = StackTrace.current.toString().trimRight().split('\n');
+ if (maxFrames != null)
+ lines = lines.take(maxFrames);
+ debugPrint(FlutterError.defaultStackFilter(lines).join('\n'));
+}
diff --git a/packages/flutter/lib/src/foundation/platform.dart b/packages/flutter/lib/src/foundation/platform.dart
index 3881055..4854155 100644
--- a/packages/flutter/lib/src/foundation/platform.dart
+++ b/packages/flutter/lib/src/foundation/platform.dart
@@ -15,15 +15,33 @@
iOS,
}
-/// The [TargetPlatform] that matches the platform on which the framework is currently executing.
+/// The [TargetPlatform] that matches the platform on which the framework is
+/// currently executing.
+///
+/// In a test environment, the platform returned is [TargetPlatform.android]
+/// regardless of the host platform. (Android was chosen because the tests were
+/// originally written assuming Android-like behavior, and we added platform
+/// adaptations for iOS later). Tests can check iOS behavior by using the
+/// platform override APIs (like in [ThemeData.platform] in the material
+/// library).
TargetPlatform get defaultTargetPlatform {
- if (Platform.isIOS || Platform.isMacOS)
- return TargetPlatform.iOS;
- if (Platform.isAndroid || Platform.isLinux)
- return TargetPlatform.android;
- throw new FlutterError(
- 'Unknown platform\n'
- '${Platform.operatingSystem} was not recognized as a target platform. '
- 'Consider updating the list of TargetPlatforms to include this platform.'
- );
+ TargetPlatform result;
+ if (Platform.isIOS || Platform.isMacOS) {
+ result = TargetPlatform.iOS;
+ } else if (Platform.isAndroid || Platform.isLinux) {
+ result = TargetPlatform.android;
+ }
+ assert(() {
+ if (Platform.environment.containsKey('FLUTTER_TEST'))
+ result = TargetPlatform.android;
+ return true;
+ });
+ if (result == null) {
+ throw new FlutterError(
+ 'Unknown platform.\n'
+ '${Platform.operatingSystem} was not recognized as a target platform. '
+ 'Consider updating the list of TargetPlatforms to include this platform.'
+ );
+ }
+ return result;
}
diff --git a/packages/flutter/lib/src/foundation/print.dart b/packages/flutter/lib/src/foundation/print.dart
index c9aecad..2d15441 100644
--- a/packages/flutter/lib/src/foundation/print.dart
+++ b/packages/flutter/lib/src/foundation/print.dart
@@ -160,10 +160,3 @@
}
}
}
-
-/// Dump the current stack to the console using [debugPrint].
-///
-/// The current stack is obtained using [StackTrace.current].
-void debugPrintStack() {
- debugPrint(StackTrace.current.toString());
-}
diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart
index d79d3d2..094b611 100644
--- a/packages/flutter/lib/src/material/app.dart
+++ b/packages/flutter/lib/src/material/app.dart
@@ -23,15 +23,6 @@
decorationStyle: TextDecorationStyle.double
);
-/// The visual and interaction design for overscroll.
-enum OverscrollStyle {
- /// Overscrolls are clamped and indicated with a glow.
- glow,
-
- /// Overscrolls are not clamped and indicated with elastic physics.
- bounce
-}
-
/// An application that uses material design.
///
/// A convenience widget that wraps a number of widgets that are commonly
@@ -60,7 +51,6 @@
this.theme,
this.home,
this.routes: const <String, WidgetBuilder>{},
- this.overscrollStyle,
this.onGenerateRoute,
this.onLocaleChanged,
this.debugShowMaterialGrid: false,
@@ -112,11 +102,6 @@
/// build the page instead.
final Map<String, WidgetBuilder> routes;
- /// The visual and interaction design for overscroll.
- ///
- /// Defaults to being adapted to the current [TargetPlatform].
- final OverscrollStyle overscrollStyle;
-
/// The route generator callback used when the app is navigated to a
/// named route.
final RouteFactory onGenerateRoute;
@@ -158,13 +143,34 @@
_MaterialAppState createState() => new _MaterialAppState();
}
-class _IndicatorScrollConfigurationDelegate extends ScrollConfigurationDelegate {
+class _ScrollLikeCupertinoDelegate extends ScrollConfigurationDelegate {
+ const _ScrollLikeCupertinoDelegate();
+
@override
- Widget wrapScrollWidget(Widget scrollWidget) => new OverscrollIndicator(child: scrollWidget);
+ TargetPlatform get platform => TargetPlatform.iOS;
+
+ @override
+ ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.iOS);
+
+ @override
+ bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
}
-final ScrollConfigurationDelegate _glowScroll = new _IndicatorScrollConfigurationDelegate();
-final ScrollConfigurationDelegate _bounceScroll = new ScrollConfigurationDelegate();
+class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate {
+ const _ScrollLikeMountainViewDelegate();
+
+ @override
+ TargetPlatform get platform => TargetPlatform.android;
+
+ @override
+ ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: TargetPlatform.android);
+
+ @override
+ Widget wrapScrollWidget(Widget scrollWidget) => new OverscrollIndicator(child: scrollWidget);
+
+ @override
+ bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
+}
class _MaterialAppState extends State<MaterialApp> {
HeroController _heroController;
@@ -195,19 +201,11 @@
}
ScrollConfigurationDelegate _getScrollDelegate(TargetPlatform platform) {
- if (config.overscrollStyle != null) {
- switch (config.overscrollStyle) {
- case OverscrollStyle.glow:
- return _glowScroll;
- case OverscrollStyle.bounce:
- return _bounceScroll;
- }
- }
switch (platform) {
case TargetPlatform.android:
- return _glowScroll;
+ return const _ScrollLikeMountainViewDelegate();
case TargetPlatform.iOS:
- return _bounceScroll;
+ return const _ScrollLikeCupertinoDelegate();
}
return null;
}
diff --git a/packages/flutter/lib/src/material/drop_down.dart b/packages/flutter/lib/src/material/drop_down.dart
index 6826960..2b3bf40 100644
--- a/packages/flutter/lib/src/material/drop_down.dart
+++ b/packages/flutter/lib/src/material/drop_down.dart
@@ -78,12 +78,21 @@
// Do not use the platform-specific default scroll configuration.
// Dropdown menus should never overscroll or display an overscroll indicator.
class _DropDownScrollConfigurationDelegate extends ScrollConfigurationDelegate {
- const _DropDownScrollConfigurationDelegate();
+ const _DropDownScrollConfigurationDelegate(this._platform);
+
+ @override
+ TargetPlatform get platform => _platform;
+ final TargetPlatform _platform;
+
+ @override
+ ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: platform);
@override
Widget wrapScrollWidget(Widget scrollWidget) => new ClampOverscrolls(value: true, child: scrollWidget);
+
+ @override
+ bool updateShouldNotify(ScrollConfigurationDelegate old) => platform != old.platform;
}
-final ScrollConfigurationDelegate _dropDownScroll = const _DropDownScrollConfigurationDelegate();
class _DropDownMenu<T> extends StatefulWidget {
_DropDownMenu({
@@ -170,7 +179,7 @@
type: MaterialType.transparency,
textStyle: route.style,
child: new ScrollConfiguration(
- delegate: _dropDownScroll,
+ delegate: new _DropDownScrollConfigurationDelegate(Theme.of(context).platform),
child: new Scrollbar(
child: new ScrollableList(
scrollableKey: config.route.scrollableKey,
diff --git a/packages/flutter/lib/src/material/input.dart b/packages/flutter/lib/src/material/input.dart
index df4365f..468de74 100644
--- a/packages/flutter/lib/src/material/input.dart
+++ b/packages/flutter/lib/src/material/input.dart
@@ -205,6 +205,7 @@
selectionColor: themeData.textSelectionColor,
selectionHandleBuilder: buildTextSelectionHandle,
selectionToolbarBuilder: buildTextSelectionToolbar,
+ platform: Theme.of(context).platform,
keyboardType: config.keyboardType,
onChanged: onChanged,
onSubmitted: onSubmitted
diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart
index a85f2d8..d28d055 100644
--- a/packages/flutter/lib/src/material/tabs.dart
+++ b/packages/flutter/lib/src/material/tabs.dart
@@ -1090,10 +1090,13 @@
@override
ExtentScrollBehavior get scrollBehavior {
- _boundedBehavior ??= new BoundedBehavior();
+ _boundedBehavior ??= new BoundedBehavior(platform: platform);
return _boundedBehavior;
}
+ @override
+ TargetPlatform get platform => Theme.of(context).platform;
+
void _initSelection(TabBarSelectionState<T> selection) {
_selection = selection;
if (_selection != null) {
diff --git a/packages/flutter/lib/src/widgets/editable.dart b/packages/flutter/lib/src/widgets/editable.dart
index f224fa4..b10e1e6 100644
--- a/packages/flutter/lib/src/widgets/editable.dart
+++ b/packages/flutter/lib/src/widgets/editable.dart
@@ -169,6 +169,7 @@
this.selectionColor,
this.selectionHandleBuilder,
this.selectionToolbarBuilder,
+ @required this.platform,
this.keyboardType,
this.onChanged,
this.onSubmitted
@@ -206,6 +207,12 @@
/// text selection (e.g. copy and paste).
final TextSelectionToolbarBuilder selectionToolbarBuilder;
+ /// The platform whose behavior should be approximated, in particular
+ /// for scroll physics. (See [ScrollBehavior.platform].)
+ ///
+ /// Must not be null.
+ final TargetPlatform platform;
+
/// The type of keyboard to use for editing the text.
final KeyboardType keyboardType;
@@ -229,7 +236,7 @@
TextSelectionOverlay _selectionOverlay;
@override
- ScrollBehavior<double, double> createScrollBehavior() => new BoundedBehavior();
+ ScrollBehavior<double, double> createScrollBehavior() => new BoundedBehavior(platform: config.platform);
@override
BoundedBehavior get scrollBehavior => super.scrollBehavior;
diff --git a/packages/flutter/lib/src/widgets/pageable_list.dart b/packages/flutter/lib/src/widgets/pageable_list.dart
index 611f60f..a6ab213 100644
--- a/packages/flutter/lib/src/widgets/pageable_list.dart
+++ b/packages/flutter/lib/src/widgets/pageable_list.dart
@@ -9,6 +9,7 @@
import 'basic.dart';
import 'framework.dart';
import 'scroll_behavior.dart';
+import 'scroll_configuration.dart';
import 'scrollable.dart';
import 'virtual_viewport.dart';
@@ -292,13 +293,18 @@
@override
ExtentScrollBehavior get scrollBehavior {
if (config.itemsWrap) {
- _unboundedBehavior ??= new UnboundedBehavior();
+ _unboundedBehavior ??= new UnboundedBehavior(platform: platform);
return _unboundedBehavior;
}
- _overscrollBehavior ??= new OverscrollBehavior();
+ _overscrollBehavior ??= new OverscrollBehavior(platform: platform);
return _overscrollBehavior;
}
+ /// Returns the style of scrolling to use.
+ ///
+ /// By default, defers to the nearest [ScrollConfiguration].
+ TargetPlatform get platform => ScrollConfiguration.of(context)?.platform;
+
@override
ExtentScrollBehavior createScrollBehavior() => scrollBehavior;
diff --git a/packages/flutter/lib/src/widgets/scroll_behavior.dart b/packages/flutter/lib/src/widgets/scroll_behavior.dart
index f286be3..51dc774 100644
--- a/packages/flutter/lib/src/widgets/scroll_behavior.dart
+++ b/packages/flutter/lib/src/widgets/scroll_behavior.dart
@@ -4,21 +4,49 @@
import 'dart:math' as math;
+import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart';
import 'package:meta/meta.dart';
-const double _kScrollDrag = 0.025;
+export 'package:flutter/foundation.dart' show TargetPlatform;
+
+const double _kScrollDragMountainView = 0.025;
+const double _kScrollDragCupertino = 0.125;
+final SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1);
+
+Simulation _createScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset, double scrollDrag) {
+ return new ScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, _kScrollSpring, scrollDrag);
+}
+
+Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
+ return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
+}
// TODO(hansmuller): Simplify these classes. We're no longer using the ScrollBehavior<T, U>
// base class directly. Only LazyBlock uses BoundedBehavior's updateExtents minScrollOffset
// parameter; simpler to move that into ExtentScrollBehavior. All of the classes should
-// be called FooScrollBehavior.
+// be called FooScrollBehavior. See https://github.com/flutter/flutter/issues/5281
/// An interface for controlling the behavior of scrollable widgets.
///
/// The type argument T is the type that describes the scroll offset.
/// The type argument U is the type that describes the scroll velocity.
abstract class ScrollBehavior<T, U> {
+ /// Abstract const constructor. This constructor enables subclasses to provide
+ /// const constructors so that they can be used in const expressions.
+ ///
+ /// The [platform] must not be null.
+ const ScrollBehavior({
+ @required this.platform
+ });
+
+ /// The platform for which physics constants should be approximated.
+ ///
+ /// This is what makes flings go further on iOS than Android.
+ ///
+ /// Must not be null.
+ final TargetPlatform platform;
+
/// Returns a simulation that propels the scrollOffset.
///
/// This function is called when a drag gesture ends.
@@ -41,6 +69,19 @@
/// Whether this scroll behavior currently permits scrolling
bool get isScrollable => true;
+ /// The scroll drag constant to use for physics simulations created by this
+ /// ScrollBehavior.
+ double get scrollDrag {
+ assert(platform != null);
+ switch (platform) {
+ case TargetPlatform.android:
+ return _kScrollDragMountainView;
+ case TargetPlatform.iOS:
+ return _kScrollDragCupertino;
+ }
+ return null;
+ }
+
@override
String toString() {
List<String> description = <String>[];
@@ -64,8 +105,15 @@
/// Creates a scroll behavior for a scrollable widget with linear extent.
/// We start with an INFINITE contentExtent so that we don't accidentally
/// clamp a scrollOffset until we receive an accurate value in updateExtents.
- ExtentScrollBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0 })
- : _contentExtent = contentExtent, _containerExtent = containerExtent;
+ ///
+ /// The extents and the [platform] must not be null.
+ ExtentScrollBehavior({
+ double contentExtent: double.INFINITY,
+ double containerExtent: 0.0,
+ @required TargetPlatform platform
+ }) : _contentExtent = contentExtent,
+ _containerExtent = containerExtent,
+ super(platform: platform);
/// The linear extent of the content inside the scrollable widget.
double get contentExtent => _contentExtent;
@@ -115,9 +163,14 @@
BoundedBehavior({
double contentExtent: double.INFINITY,
double containerExtent: 0.0,
- double minScrollOffset: 0.0
+ double minScrollOffset: 0.0,
+ @required TargetPlatform platform
}) : _minScrollOffset = minScrollOffset,
- super(contentExtent: contentExtent, containerExtent: containerExtent);
+ super(
+ contentExtent: contentExtent,
+ containerExtent: containerExtent,
+ platform: platform
+ );
double _minScrollOffset;
@@ -151,25 +204,23 @@
}
}
-Simulation _createScrollSimulation(double position, double velocity, double minScrollOffset, double maxScrollOffset) {
- final SpringDescription spring = new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 170.0, ratio: 1.1);
- return new ScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, spring, _kScrollDrag);
-}
-
-Simulation _createSnapScrollSimulation(double startOffset, double endOffset, double startVelocity, double endVelocity) {
- return new FrictionSimulation.through(startOffset, endOffset, startVelocity, endVelocity);
-}
-
-/// A scroll behavior that does not prevent the user from exeeding scroll bounds.
+/// A scroll behavior that does not prevent the user from exceeding scroll bounds.
class UnboundedBehavior extends ExtentScrollBehavior {
/// Creates a scroll behavior with no scrolling limits.
- UnboundedBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0 })
- : super(contentExtent: contentExtent, containerExtent: containerExtent);
+ UnboundedBehavior({
+ double contentExtent: double.INFINITY,
+ double containerExtent: 0.0,
+ @required TargetPlatform platform
+ }) : super(
+ contentExtent: contentExtent,
+ containerExtent: containerExtent,
+ platform: platform
+ );
@override
Simulation createScrollSimulation(double position, double velocity) {
return new BoundedFrictionSimulation(
- _kScrollDrag, position, velocity, double.NEGATIVE_INFINITY, double.INFINITY
+ scrollDrag, position, velocity, double.NEGATIVE_INFINITY, double.INFINITY
);
}
@@ -193,12 +244,21 @@
/// A scroll behavior that lets the user scroll beyond the scroll bounds with some resistance.
class OverscrollBehavior extends BoundedBehavior {
/// Creates a scroll behavior that resists, but does not prevent, scrolling beyond its limits.
- OverscrollBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0, double minScrollOffset: 0.0 })
- : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
+ OverscrollBehavior({
+ double contentExtent: double.INFINITY,
+ double containerExtent: 0.0,
+ double minScrollOffset: 0.0,
+ @required TargetPlatform platform
+ }) : super(
+ contentExtent: contentExtent,
+ containerExtent: containerExtent,
+ minScrollOffset: minScrollOffset,
+ platform: platform
+ );
@override
Simulation createScrollSimulation(double position, double velocity) {
- return _createScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset);
+ return _createScrollSimulation(position, velocity, minScrollOffset, maxScrollOffset, scrollDrag);
}
@override
@@ -227,8 +287,17 @@
/// A scroll behavior that lets the user scroll beyond the scroll bounds only when the bounds are disjoint.
class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
/// Creates a scroll behavior that allows overscrolling only when some amount of scrolling is already possible.
- OverscrollWhenScrollableBehavior({ double contentExtent: double.INFINITY, double containerExtent: 0.0, double minScrollOffset: 0.0 })
- : super(contentExtent: contentExtent, containerExtent: containerExtent, minScrollOffset: minScrollOffset);
+ OverscrollWhenScrollableBehavior({
+ double contentExtent: double.INFINITY,
+ double containerExtent: 0.0,
+ double minScrollOffset: 0.0,
+ @required TargetPlatform platform
+ }) : super(
+ contentExtent: contentExtent,
+ containerExtent: containerExtent,
+ minScrollOffset: minScrollOffset,
+ platform: platform
+ );
@override
bool get isScrollable => contentExtent > containerExtent;
diff --git a/packages/flutter/lib/src/widgets/scroll_configuration.dart b/packages/flutter/lib/src/widgets/scroll_configuration.dart
index ed54658..dda1ca3 100644
--- a/packages/flutter/lib/src/widgets/scroll_configuration.dart
+++ b/packages/flutter/lib/src/widgets/scroll_configuration.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:meta/meta.dart';
+import 'package:flutter/foundation.dart';
import 'framework.dart';
import 'scroll_behavior.dart';
@@ -10,13 +11,18 @@
/// Controls how [Scrollable] widgets in a subtree behave.
///
/// Used by [ScrollConfiguration].
-class ScrollConfigurationDelegate {
- /// Creates a delegate with sensible default behaviors.
+abstract class ScrollConfigurationDelegate {
+ /// Abstract const constructor. This constructor enables subclasses to provide
+ /// const constructors so that they can be used in const expressions.
const ScrollConfigurationDelegate();
+ /// Returns the platform whose scroll physics should be approximated. See
+ /// [ScrollBehavior.platform].
+ TargetPlatform get platform;
+
/// Returns the ScrollBehavior to be used by generic scrolling containers like
- /// [Block]. Returns a new [OverscrollWhenScrollableBehavior] by default.
- ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
+ /// [Block].
+ ExtentScrollBehavior createScrollBehavior();
/// Generic scrolling containers like [Block] will apply this function to the
/// Scrollable they create. It can be used to add widgets that wrap the
@@ -24,9 +30,22 @@
/// [scrollWidget] parameter is returned unchanged.
Widget wrapScrollWidget(Widget scrollWidget) => scrollWidget;
- /// Overrides should return true if the this ScrollConfigurationDelegate has
- /// changed in a way that requires rebuilding its scrolling container descendants.
- /// Returns false by default.
+ /// Overrides should return true if this ScrollConfigurationDelegate differs
+ /// from the provided old delegate in a way that requires rebuilding its
+ /// scrolling container descendants.
+ bool updateShouldNotify(ScrollConfigurationDelegate old);
+}
+
+class _DefaultScrollConfigurationDelegate extends ScrollConfigurationDelegate {
+ const _DefaultScrollConfigurationDelegate();
+
+ @override
+ TargetPlatform get platform => defaultTargetPlatform;
+
+ @override
+ ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior(platform: platform);
+
+ @override
bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
}
@@ -49,7 +68,7 @@
@required Widget child
}) : super(key: key, child: child);
- static const ScrollConfigurationDelegate _defaultDelegate = const ScrollConfigurationDelegate();
+ static const ScrollConfigurationDelegate _defaultDelegate = const _DefaultScrollConfigurationDelegate();
/// Defines the ScrollBehavior and scrollable wrapper for descendants.
final ScrollConfigurationDelegate delegate;
@@ -57,8 +76,10 @@
/// The delegate property of the closest instance of this class that encloses
/// the given context.
///
- /// If no such instance exists, returns an instance of the
- /// [ScrollConfigurationDelegate] base class.
+ /// If no such instance exists, returns a default
+ /// [ScrollConfigurationDelegate] that approximates the scrolling physics of
+ /// the current platform (see [defaultTargetPlatform]) using a
+ /// [OverscrollWhenScrollableBehavior] behavior model.
static ScrollConfigurationDelegate of(BuildContext context) {
ScrollConfiguration configuration = context.inheritFromWidgetOfExactType(ScrollConfiguration);
return configuration?.delegate ?? _defaultDelegate;
@@ -66,7 +87,7 @@
/// A utility function that calls [ScrollConfigurationDelegate.wrapScrollWidget].
static Widget wrap(BuildContext context, Widget scrollWidget) {
- return of(context).wrapScrollWidget(scrollWidget);
+ return ScrollConfiguration.of(context).wrapScrollWidget(scrollWidget);
}
@override
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index b502a49..6ba23b2 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -6,8 +6,8 @@
import 'dart:math' as math;
import 'dart:ui' as ui show window;
-import 'package:flutter/physics.dart';
import 'package:flutter/gestures.dart';
+import 'package:flutter/physics.dart';
import 'package:meta/meta.dart';
import 'basic.dart';
diff --git a/packages/flutter/test/widget/lazy_block_fling_test.dart b/packages/flutter/test/widget/lazy_block_fling_test.dart
new file mode 100644
index 0000000..8e1cb3b
--- /dev/null
+++ b/packages/flutter/test/widget/lazy_block_fling_test.dart
@@ -0,0 +1,42 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/material.dart';
+
+const double kHeight = 10.0;
+const double kFlingOffset = kHeight * 20.0;
+
+class TestDelegate extends LazyBlockDelegate {
+ @override
+ Widget buildItem(BuildContext context, int index) {
+ return new Container(height: kHeight);
+ }
+
+ @override
+ double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) {
+ return double.INFINITY;
+ }
+
+ @override
+ bool shouldRebuild(LazyBlockDelegate oldDelegate) => false;
+}
+
+double currentOffset;
+
+void main() {
+ testWidgets('Flings don\'t stutter', (WidgetTester tester) async {
+ await tester.pumpWidget(new LazyBlock(
+ delegate: new TestDelegate(),
+ onScroll: (double scrollOffset) { currentOffset = scrollOffset; },
+ ));
+ await tester.fling(find.byType(LazyBlock), const Offset(0.0, -kFlingOffset), 1000.0);
+ expect(currentOffset, kFlingOffset);
+ while (tester.binding.transientCallbackCount > 0) {
+ double lastOffset = currentOffset;
+ await tester.pump(const Duration(milliseconds: 20));
+ expect(currentOffset, greaterThan(lastOffset));
+ }
+ }, skip: true); // see https://github.com/flutter/flutter/issues/5339
+}
diff --git a/packages/flutter/test/widget/scroll_behavior_test.dart b/packages/flutter/test/widget/scroll_behavior_test.dart
index 30f65d9..79dd83b 100644
--- a/packages/flutter/test/widget/scroll_behavior_test.dart
+++ b/packages/flutter/test/widget/scroll_behavior_test.dart
@@ -10,7 +10,8 @@
BoundedBehavior behavior = new BoundedBehavior(
contentExtent: 150.0,
containerExtent: 75.0,
- minScrollOffset: -100.0
+ minScrollOffset: -100.0,
+ platform: TargetPlatform.iOS
);
expect(behavior.minScrollOffset, equals(-100.0));
expect(behavior.maxScrollOffset, equals(-25.0));
diff --git a/packages/flutter/test/widget/scroll_notification_test.dart b/packages/flutter/test/widget/scroll_notification_test.dart
index be1d41f..dd64e93 100644
--- a/packages/flutter/test/widget/scroll_notification_test.dart
+++ b/packages/flutter/test/widget/scroll_notification_test.dart
@@ -6,7 +6,7 @@
import 'package:flutter/widgets.dart';
void main() {
- testWidgets('Scroll notifcation basics', (WidgetTester tester) async {
+ testWidgets('Scroll notification basics', (WidgetTester tester) async {
ScrollNotification notification;
await tester.pumpWidget(new NotificationListener<ScrollNotification>(
@@ -48,7 +48,7 @@
expect(notification.dragEndDetails.velocity, equals(Velocity.zero));
});
- testWidgets('Scroll notifcation depth', (WidgetTester tester) async {
+ testWidgets('Scroll notification depth', (WidgetTester tester) async {
final List<ScrollNotificationKind> depth0Kinds = <ScrollNotificationKind>[];
final List<ScrollNotificationKind> depth1Kinds = <ScrollNotificationKind>[];
final List<int> depth0Values = <int>[];
diff --git a/packages/flutter/test/widget/scrollable_fling_test.dart b/packages/flutter/test/widget/scrollable_fling_test.dart
new file mode 100644
index 0000000..108da0b
--- /dev/null
+++ b/packages/flutter/test/widget/scrollable_fling_test.dart
@@ -0,0 +1,61 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/material.dart';
+
+class TestDelegate extends LazyBlockDelegate {
+ @override
+ Widget buildItem(BuildContext context, int index) {
+ return new Text('$index');
+ }
+
+ @override
+ double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) {
+ return double.INFINITY;
+ }
+
+ @override
+ bool shouldRebuild(LazyBlockDelegate oldDelegate) => false;
+}
+
+double currentOffset;
+
+Future<Null> pumpTest(WidgetTester tester, TargetPlatform platform) async {
+ await tester.pumpWidget(new Container());
+ await tester.pumpWidget(new MaterialApp(
+ theme: new ThemeData(
+ platform: platform
+ ),
+ home: new LazyBlock(
+ delegate: new TestDelegate(),
+ onScroll: (double scrollOffset) { currentOffset = scrollOffset; },
+ ),
+ ));
+ return null;
+}
+
+const double dragOffset = 213.82;
+
+void main() {
+ testWidgets('Flings on different platforms', (WidgetTester tester) async {
+ await pumpTest(tester, TargetPlatform.android);
+ await tester.fling(find.byType(LazyBlock), const Offset(0.0, -dragOffset), 1000.0);
+ expect(currentOffset, dragOffset);
+ await tester.pump(); // trigger fling
+ expect(currentOffset, dragOffset);
+ await tester.pump(const Duration(seconds: 5));
+ final double result1 = currentOffset;
+
+ await pumpTest(tester, TargetPlatform.iOS);
+ await tester.fling(find.byType(LazyBlock), const Offset(0.0, -dragOffset), 1000.0);
+ expect(currentOffset, dragOffset);
+ await tester.pump(); // trigger fling
+ expect(currentOffset, dragOffset);
+ await tester.pump(const Duration(seconds: 5));
+ final double result2 = currentOffset;
+
+ expect(result1, lessThan(result2)); // iOS (result2) is slipperier than Android (result1)
+ });
+}
diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart
index 7899c44..2cd215b 100644
--- a/packages/flutter_test/lib/src/controller.dart
+++ b/packages/flutter_test/lib/src/controller.dart
@@ -264,13 +264,29 @@
///
/// If the middle of the widget is not exposed, this might send
/// events to another object.
- Future<Null> fling(Finder finder, Offset offset, double velocity, { int pointer: 1 }) {
- return flingFrom(getCenter(finder), offset, velocity, pointer: pointer);
+ ///
+ /// This can pump frames. See [flingFrom] for a discussion of how the
+ /// `offset`, `velocity` and `frameInterval` arguments affect this.
+ Future<Null> fling(Finder finder, Offset offset, double velocity, { int pointer: 1, Duration frameInterval: const Duration(milliseconds: 16) }) {
+ return flingFrom(getCenter(finder), offset, velocity, pointer: pointer, frameInterval: frameInterval);
}
/// Attempts a fling gesture starting from the given location,
/// moving the given distance, reaching the given velocity.
- Future<Null> flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1 }) {
+ ///
+ /// Exactly 50 pointer events are synthesized.
+ ///
+ /// The offset and velocity control the interval between each pointer event.
+ /// For example, if the offset is 200 pixels, and the velocity is 800 pixels
+ /// per second, the pointer events will be sent for each increment of 4 pixels
+ /// (200/50), over 250ms (200/800), meaning events will be sent every 1.25ms
+ /// (250/200).
+ ///
+ /// To make tests more realistic, frames may be pumped during this time (using
+ /// calls to [pump]). If the total duration is longer than `frameInterval`,
+ /// then one frame is pumped each time that amount of time elapses while
+ /// sending events, or each time an event is synthesised, whichever is rarer.
+ Future<Null> flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1, Duration frameInterval: const Duration(milliseconds: 16) }) {
return TestAsyncUtils.guard(() async {
assert(offset.distance > 0.0);
assert(velocity != 0.0); // velocity is pixels/second
@@ -279,17 +295,32 @@
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
double timeStamp = 0.0;
+ double lastTimeStamp = timeStamp;
await sendEventToBinding(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
- for (int i = 0; i <= kMoveCount; i++) {
+ for (int i = 0; i <= kMoveCount; i += 1) {
final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount);
await sendEventToBinding(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += timeStampDelta;
+ if (timeStamp - lastTimeStamp > frameInterval.inMilliseconds) {
+ await pump(new Duration(milliseconds: (timeStamp - lastTimeStamp).truncate()));
+ lastTimeStamp = timeStamp;
+ }
}
await sendEventToBinding(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
return null;
});
}
+ /// Called to indicate that time should advance.
+ ///
+ /// This is invoked by [flingFrom], for instance, so that the sequence of
+ /// pointer events occurs over time.
+ ///
+ /// The default implementation does nothing.
+ ///
+ /// The [WidgetTester] subclass implements this by deferring to the [binding].
+ Future<Null> pump(Duration duration) => new Future<Null>.value(null);
+
/// Attempts to drag the given widget by the given offset, by
/// starting a drag in the middle of the widget.
///
diff --git a/packages/flutter_test/lib/src/test_async_utils.dart b/packages/flutter_test/lib/src/test_async_utils.dart
index 6aad085..e0726be 100644
--- a/packages/flutter_test/lib/src/test_async_utils.dart
+++ b/packages/flutter_test/lib/src/test_async_utils.dart
@@ -67,7 +67,7 @@
_AsyncScope scope = new _AsyncScope(StackTrace.current, zone);
_scopeStack.add(scope);
Future<Null> result = scope.zone.run(body);
- result = result.whenComplete(() {
+ void completionHandler(dynamic error, StackTrace stack) {
assert(_scopeStack.isNotEmpty);
assert(_scopeStack.contains(scope));
bool leaked = false;
@@ -77,8 +77,10 @@
closedScope = _scopeStack.removeLast();
if (closedScope == scope)
break;
- leaked = true;
- message.writeln('Asynchronous call to guarded function leaked. You must use "await" with all Future-returning test APIs.');
+ if (!leaked) {
+ message.writeln('Asynchronous call to guarded function leaked. You must use "await" with all Future-returning test APIs.');
+ leaked = true;
+ }
final _StackEntry originalGuarder = _findResponsibleMethod(closedScope.creationStack, 'guard', message);
if (originalGuarder != null) {
message.writeln(
@@ -90,10 +92,20 @@
);
}
}
- if (leaked)
+ if (leaked) {
+ if (error != null) {
+ message.writeln('An uncaught exception may have caused the guarded function leak. The exception was:');
+ message.writeln('$error');
+ message.writeln('The stack trace associated with this exception was:');
+ FlutterError.defaultStackFilter(stack.toString().trimRight().split('\n')).forEach(message.writeln);
+ }
throw new FlutterError(message.toString().trimRight());
- });
- return result;
+ }
+ }
+ return result.then(
+ (Null value) => completionHandler(null, null),
+ onError: completionHandler
+ );
}
static Zone get _currentScopeZone {
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 06fdbe5..8b96c59 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -36,9 +36,9 @@
///
/// Example:
///
-/// testWidgets('MyWidget', (WidgetTester tester) {
-/// tester.pumpWidget(new MyWidget());
-/// tester.tap(find.text('Save'));
+/// testWidgets('MyWidget', (WidgetTester tester) async {
+/// await tester.pumpWidget(new MyWidget());
+/// await tester.tap(find.text('Save'));
/// expect(tester, hasWidget(find.text('Success')));
/// });
void testWidgets(String description, WidgetTesterCallback callback, {
@@ -77,12 +77,12 @@
///
/// main() async {
/// assert(false); // fail in checked mode
-/// await benchmarkWidgets((WidgetTester tester) {
-/// tester.pumpWidget(new MyWidget());
+/// await benchmarkWidgets((WidgetTester tester) async {
+/// await tester.pumpWidget(new MyWidget());
/// final Stopwatch timer = new Stopwatch()..start();
/// for (int index = 0; index < 10000; index += 1) {
-/// tester.tap(find.text('Tap me'));
-/// tester.pump();
+/// await tester.tap(find.text('Tap me'));
+/// await tester.pump();
/// }
/// timer.stop();
/// debugPrint('Time taken: ${timer.elapsedMilliseconds}ms');
@@ -99,7 +99,7 @@
print('│ enabled will not accurately reflect the performance │');
print('│ that will be experienced by end users using release ╎');
print('│ builds. Benchmarks should be run using this command ┆');
- print('│ line: flutter run --release -t benchmark.dart ┊');
+ print('│ line: flutter run --release benchmark.dart ┊');
print('│ ');
print('└─────────────────────────────────────────────────╌┄┈ 🐢');
return true;
@@ -172,6 +172,7 @@
///
/// This is a convenience function that just calls
/// [TestWidgetsFlutterBinding.pump].
+ @override
Future<Null> pump([
Duration duration,
EnginePhase phase = EnginePhase.sendSemanticsTree