Unverified Commit 70b19ff9 authored by Callum Moffat's avatar Callum Moffat Committed by GitHub

Add macOS-specific scroll physics (#108298)

parent df110ef0
...@@ -480,6 +480,9 @@ class CupertinoScrollBehavior extends ScrollBehavior { ...@@ -480,6 +480,9 @@ class CupertinoScrollBehavior extends ScrollBehavior {
@override @override
ScrollPhysics getScrollPhysics(BuildContext context) { ScrollPhysics getScrollPhysics(BuildContext context) {
if (getPlatform(context) == TargetPlatform.macOS) {
return const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast);
}
return const BouncingScrollPhysics(); return const BouncingScrollPhysics();
} }
} }
......
...@@ -373,3 +373,61 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker { ...@@ -373,3 +373,61 @@ class IOSScrollViewFlingVelocityTracker extends VelocityTracker {
} }
} }
} }
/// A [VelocityTracker] subclass that provides a close approximation of macOS
/// scroll view's velocity estimation strategy.
///
/// The estimated velocity reported by this class is a close approximation of
/// the velocity a macOS scroll view would report with the same
/// [PointerMoveEvent]s, when the touch that initiates a fling is released.
///
/// This class differs from the [VelocityTracker] class in that it uses weighted
/// average of the latest few velocity samples of the tracked pointer, instead
/// of doing a linear regression on a relatively large amount of data points, to
/// estimate the velocity of the tracked pointer. Adding data points and
/// estimating the velocity are both cheap.
///
/// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. The
/// estimated velocity is typically used as the initial flinging velocity of a
/// `Scrollable`, when its drag gesture ends.
class MacOSScrollViewFlingVelocityTracker extends IOSScrollViewFlingVelocityTracker {
/// Create a new MacOSScrollViewFlingVelocityTracker.
MacOSScrollViewFlingVelocityTracker(super.kind);
@override
VelocityEstimate getVelocityEstimate() {
// The velocity estimated using this expression is an approximation of the
// scroll velocity of a macOS scroll view at the moment the user touch was
// released.
final Offset estimatedVelocity = _previousVelocityAt(-2) * 0.15
+ _previousVelocityAt(-1) * 0.65
+ _previousVelocityAt(0) * 0.2;
final _PointAtTime? newestSample = _touchSamples[_index];
_PointAtTime? oldestNonNullSample;
for (int i = 1; i <= IOSScrollViewFlingVelocityTracker._sampleSize; i += 1) {
oldestNonNullSample = _touchSamples[(_index + i) % IOSScrollViewFlingVelocityTracker._sampleSize];
if (oldestNonNullSample != null) {
break;
}
}
if (oldestNonNullSample == null || newestSample == null) {
assert(false, 'There must be at least 1 point in _touchSamples: $_touchSamples');
return const VelocityEstimate(
pixelsPerSecond: Offset.zero,
confidence: 0.0,
duration: Duration.zero,
offset: Offset.zero,
);
} else {
return VelocityEstimate(
pixelsPerSecond: estimatedVelocity,
confidence: 1.0,
duration: newestSample.time - oldestNonNullSample.time,
offset: newestSample.point - oldestNonNullSample.point,
);
}
}
}
...@@ -10,6 +10,22 @@ import 'simulation.dart'; ...@@ -10,6 +10,22 @@ import 'simulation.dart';
export 'tolerance.dart' show Tolerance; export 'tolerance.dart' show Tolerance;
/// Numerically determine the input value which produces output value [target]
/// for a function [f], given its first-derivative [df].
double _newtonsMethod({
required double initialGuess,
required double target,
required double Function(double) f,
required double Function(double) df,
required int iterations
}) {
double guess = initialGuess;
for (int i = 0; i < iterations; i++) {
guess = guess - (f(guess) - target) / df(guess);
}
return guess;
}
/// A simulation that applies a drag to slow a particle down. /// A simulation that applies a drag to slow a particle down.
/// ///
/// Models a particle affected by fluid drag, e.g. air resistance. /// Models a particle affected by fluid drag, e.g. air resistance.
...@@ -26,10 +42,20 @@ class FrictionSimulation extends Simulation { ...@@ -26,10 +42,20 @@ class FrictionSimulation extends Simulation {
double position, double position,
double velocity, { double velocity, {
super.tolerance, super.tolerance,
double constantDeceleration = 0
}) : _drag = drag, }) : _drag = drag,
_dragLog = math.log(drag), _dragLog = math.log(drag),
_x = position, _x = position,
_v = velocity; _v = velocity,
_constantDeceleration = constantDeceleration * velocity.sign {
_finalTime = _newtonsMethod(
initialGuess: 0,
target: 0,
f: dx,
df: (double time) => (_v * math.pow(_drag, time) * _dragLog) - _constantDeceleration,
iterations: 10
);
}
/// Creates a new friction simulation with its fluid drag coefficient (_cₓ_) set so /// Creates a new friction simulation with its fluid drag coefficient (_cₓ_) set so
/// as to ensure that the simulation starts and ends at the specified /// as to ensure that the simulation starts and ends at the specified
...@@ -58,6 +84,12 @@ class FrictionSimulation extends Simulation { ...@@ -58,6 +84,12 @@ class FrictionSimulation extends Simulation {
final double _dragLog; final double _dragLog;
final double _x; final double _x;
final double _v; final double _v;
final double _constantDeceleration;
// The time at which the simulation should be stopped.
// This is needed when constantDeceleration is not zero (on Desktop), when
// using the pure friction simulation, acceleration naturally reduces to zero
// and creates a stopping point.
double _finalTime = double.infinity; // needs to be infinity for newtonsMethod call in constructor.
// Return the drag value for a FrictionSimulation whose x() and dx() values pass // Return the drag value for a FrictionSimulation whose x() and dx() values pass
// through the specified start and end position/velocity values. // through the specified start and end position/velocity values.
...@@ -71,13 +103,28 @@ class FrictionSimulation extends Simulation { ...@@ -71,13 +103,28 @@ class FrictionSimulation extends Simulation {
} }
@override @override
double x(double time) => _x + _v * math.pow(_drag, time) / _dragLog - _v / _dragLog; double x(double time) {
if (time > _finalTime) {
return finalX;
}
return _x + _v * math.pow(_drag, time) / _dragLog - _v / _dragLog - ((_constantDeceleration / 2) * time * time);
}
@override @override
double dx(double time) => _v * math.pow(_drag, time); double dx(double time) {
if (time > _finalTime) {
return 0;
}
return _v * math.pow(_drag, time) - _constantDeceleration * time;
}
/// The value of [x] at `double.infinity`. /// The value of [x] at `double.infinity`.
double get finalX => _x - _v / _dragLog; double get finalX {
if (_constantDeceleration == 0) {
return _x - _v / _dragLog;
}
return x(_finalTime);
}
/// The time at which the value of `x(time)` will equal [x]. /// The time at which the value of `x(time)` will equal [x].
/// ///
...@@ -89,11 +136,19 @@ class FrictionSimulation extends Simulation { ...@@ -89,11 +136,19 @@ class FrictionSimulation extends Simulation {
if (_v == 0.0 || (_v > 0 ? (x < _x || x > finalX) : (x > _x || x < finalX))) { if (_v == 0.0 || (_v > 0 ? (x < _x || x > finalX) : (x > _x || x < finalX))) {
return double.infinity; return double.infinity;
} }
return math.log(_dragLog * (x - _x) / _v + 1.0) / _dragLog; return _newtonsMethod(
target: x,
initialGuess: 0,
f: this.x,
df: dx,
iterations: 10
);
} }
@override @override
bool isDone(double time) => dx(time).abs() < tolerance.velocity; bool isDone(double time) {
return dx(time).abs() < tolerance.velocity;
}
@override @override
String toString() => '${objectRuntimeType(this, 'FrictionSimulation')}(cₓ: ${_drag.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)})'; String toString() => '${objectRuntimeType(this, 'FrictionSimulation')}(cₓ: ${_drag.toStringAsFixed(1)}, x₀: ${_x.toStringAsFixed(1)}, dx₀: ${_v.toStringAsFixed(1)})';
......
...@@ -219,8 +219,9 @@ class ScrollBehavior { ...@@ -219,8 +219,9 @@ class ScrollBehavior {
GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) { GestureVelocityTrackerBuilder velocityTrackerBuilder(BuildContext context) {
switch (getPlatform(context)) { switch (getPlatform(context)) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS:
return (PointerEvent event) => IOSScrollViewFlingVelocityTracker(event.kind); return (PointerEvent event) => IOSScrollViewFlingVelocityTracker(event.kind);
case TargetPlatform.macOS:
return (PointerEvent event) => MacOSScrollViewFlingVelocityTracker(event.kind);
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
...@@ -230,6 +231,10 @@ class ScrollBehavior { ...@@ -230,6 +231,10 @@ class ScrollBehavior {
} }
static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics()); static const ScrollPhysics _bouncingPhysics = BouncingScrollPhysics(parent: RangeMaintainingScrollPhysics());
static const ScrollPhysics _bouncingDesktopPhysics = BouncingScrollPhysics(
decelerationRate: ScrollDecelerationRate.fast,
parent: RangeMaintainingScrollPhysics()
);
static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics()); static const ScrollPhysics _clampingPhysics = ClampingScrollPhysics(parent: RangeMaintainingScrollPhysics());
/// The scroll physics to use for the platform given by [getPlatform]. /// The scroll physics to use for the platform given by [getPlatform].
...@@ -240,8 +245,9 @@ class ScrollBehavior { ...@@ -240,8 +245,9 @@ class ScrollBehavior {
ScrollPhysics getScrollPhysics(BuildContext context) { ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) { switch (getPlatform(context)) {
case TargetPlatform.iOS: case TargetPlatform.iOS:
case TargetPlatform.macOS:
return _bouncingPhysics; return _bouncingPhysics;
case TargetPlatform.macOS:
return _bouncingDesktopPhysics;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
case TargetPlatform.linux: case TargetPlatform.linux:
......
...@@ -16,6 +16,17 @@ import 'scroll_simulation.dart'; ...@@ -16,6 +16,17 @@ import 'scroll_simulation.dart';
export 'package:flutter/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance; export 'package:flutter/physics.dart' show ScrollSpringSimulation, Simulation, Tolerance;
/// The rate at which scroll momentum will be decelerated.
enum ScrollDecelerationRate {
/// Standard deceleration, aligned with mobile software expectations.
normal,
/// Increased deceleration, aligned with desktop software expectations.
///
/// Appropriate for use with input devices more precise than touch screens,
/// such as trackpads or mouse wheels.
fast
}
// Examples can assume: // Examples can assume:
// class FooScrollPhysics extends ScrollPhysics { // class FooScrollPhysics extends ScrollPhysics {
// const FooScrollPhysics({ super.parent }); // const FooScrollPhysics({ super.parent });
...@@ -608,7 +619,13 @@ class RangeMaintainingScrollPhysics extends ScrollPhysics { ...@@ -608,7 +619,13 @@ class RangeMaintainingScrollPhysics extends ScrollPhysics {
/// of different types to get the desired scroll physics. /// of different types to get the desired scroll physics.
class BouncingScrollPhysics extends ScrollPhysics { class BouncingScrollPhysics extends ScrollPhysics {
/// Creates scroll physics that bounce back from the edge. /// Creates scroll physics that bounce back from the edge.
const BouncingScrollPhysics({ super.parent }); const BouncingScrollPhysics({
this.decelerationRate = ScrollDecelerationRate.normal,
super.parent,
});
/// Used to determine parameters for friction simulations.
final ScrollDecelerationRate decelerationRate;
@override @override
BouncingScrollPhysics applyTo(ScrollPhysics? ancestor) { BouncingScrollPhysics applyTo(ScrollPhysics? ancestor) {
...@@ -623,7 +640,14 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -623,7 +640,14 @@ class BouncingScrollPhysics extends ScrollPhysics {
/// This factor starts at 0.52 and progressively becomes harder to overscroll /// This factor starts at 0.52 and progressively becomes harder to overscroll
/// as more of the area past the edge is dragged in (represented by an increasing /// as more of the area past the edge is dragged in (represented by an increasing
/// `overscrollFraction` which starts at 0 when there is no overscroll). /// `overscrollFraction` which starts at 0 when there is no overscroll).
double frictionFactor(double overscrollFraction) => 0.52 * math.pow(1 - overscrollFraction, 2); double frictionFactor(double overscrollFraction) {
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
return 0.07 * math.pow(1 - overscrollFraction, 2);
case ScrollDecelerationRate.normal:
return 0.52 * math.pow(1 - overscrollFraction, 2);
}
}
@override @override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
...@@ -670,6 +694,15 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -670,6 +694,15 @@ class BouncingScrollPhysics extends ScrollPhysics {
Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance; final Tolerance tolerance = this.tolerance;
if (velocity.abs() >= tolerance.velocity || position.outOfRange) { if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
double constantDeceleration;
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
constantDeceleration = 1400;
break;
case ScrollDecelerationRate.normal:
constantDeceleration = 0;
break;
}
return BouncingScrollSimulation( return BouncingScrollSimulation(
spring: spring, spring: spring,
position: position.pixels, position: position.pixels,
...@@ -677,6 +710,7 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -677,6 +710,7 @@ class BouncingScrollPhysics extends ScrollPhysics {
leadingExtent: position.minScrollExtent, leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent, trailingExtent: position.maxScrollExtent,
tolerance: tolerance, tolerance: tolerance,
constantDeceleration: constantDeceleration
); );
} }
return null; return null;
...@@ -711,6 +745,30 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -711,6 +745,30 @@ class BouncingScrollPhysics extends ScrollPhysics {
// from the natural motion of lifting the finger after a scroll. // from the natural motion of lifting the finger after a scroll.
@override @override
double get dragStartDistanceMotionThreshold => 3.5; double get dragStartDistanceMotionThreshold => 3.5;
@override
double get maxFlingVelocity {
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
return kMaxFlingVelocity * 8.0;
case ScrollDecelerationRate.normal:
return super.maxFlingVelocity;
}
}
@override
SpringDescription get spring {
switch (decelerationRate) {
case ScrollDecelerationRate.fast:
return SpringDescription.withDampingRatio(
mass: 0.3,
stiffness: 75.0,
ratio: 1.3,
);
case ScrollDecelerationRate.normal:
return super.spring;
}
}
} }
/// Scroll physics for environments that prevent the scroll offset from reaching /// Scroll physics for environments that prevent the scroll offset from reaching
......
...@@ -34,6 +34,7 @@ class BouncingScrollSimulation extends Simulation { ...@@ -34,6 +34,7 @@ class BouncingScrollSimulation extends Simulation {
required this.leadingExtent, required this.leadingExtent,
required this.trailingExtent, required this.trailingExtent,
required this.spring, required this.spring,
double constantDeceleration = 0,
super.tolerance, super.tolerance,
}) : assert(position != null), }) : assert(position != null),
assert(velocity != null), assert(velocity != null),
...@@ -50,7 +51,7 @@ class BouncingScrollSimulation extends Simulation { ...@@ -50,7 +51,7 @@ class BouncingScrollSimulation extends Simulation {
} else { } else {
// Taken from UIScrollView.decelerationRate (.normal = 0.998) // Taken from UIScrollView.decelerationRate (.normal = 0.998)
// 0.998^1000 = ~0.135 // 0.998^1000 = ~0.135
_frictionSimulation = FrictionSimulation(0.135, position, velocity); _frictionSimulation = FrictionSimulation(0.135, position, velocity, constantDeceleration: constantDeceleration);
final double finalX = _frictionSimulation.finalX; final double finalX = _frictionSimulation.finalX;
if (velocity > 0.0 && finalX > trailingExtent) { if (velocity > 0.0 && finalX > trailingExtent) {
_springTime = _frictionSimulation.timeAtX(trailingExtent); _springTime = _frictionSimulation.timeAtX(trailingExtent);
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -397,9 +398,11 @@ void main() { ...@@ -397,9 +398,11 @@ void main() {
warnIfMissed: false, // has an IgnorePointer warnIfMissed: false, // has an IgnorePointer
); );
if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) {
// Should have been flung far enough that even the first item goes off // Should have been flung far enough that even the first item goes off
// screen and gets removed. // screen and gets removed.
expect(find.widgetWithText(SizedBox, '0').evaluate().isEmpty, true); expect(find.widgetWithText(SizedBox, '0').evaluate().isEmpty, true);
}
expect( expect(
selectedItems, selectedItems,
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -150,13 +151,25 @@ void main() { ...@@ -150,13 +151,25 @@ void main() {
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value. refreshIndicatorExtent: 60, // default value.
), ),
matchesBuilder( if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) matchesBuilder(
refreshState: RefreshIndicatorMode.drag,
pulledExtent: moreOrLessEquals(48.07979523362715),
refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value.
)
else matchesBuilder(
refreshState: RefreshIndicatorMode.drag, refreshState: RefreshIndicatorMode.drag,
pulledExtent: moreOrLessEquals(48.36801747187993), pulledExtent: moreOrLessEquals(48.36801747187993),
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value. refreshIndicatorExtent: 60, // default value.
), ),
matchesBuilder( if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) matchesBuilder(
refreshState: RefreshIndicatorMode.drag,
pulledExtent: moreOrLessEquals(43.98499220391114),
refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value.
)
else matchesBuilder(
refreshState: RefreshIndicatorMode.drag, refreshState: RefreshIndicatorMode.drag,
pulledExtent: moreOrLessEquals(44.63031931875867), pulledExtent: moreOrLessEquals(44.63031931875867),
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
...@@ -199,7 +212,12 @@ void main() { ...@@ -199,7 +212,12 @@ void main() {
await tester.pump(); await tester.pump();
await gesture.moveBy(const Offset(0.0, -30.0)); await gesture.moveBy(const Offset(0.0, -30.0));
await tester.pump(); await tester.pump();
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await gesture.moveBy(const Offset(0.0, 70.0));
}
else {
await gesture.moveBy(const Offset(0.0, 50.0)); await gesture.moveBy(const Offset(0.0, 50.0));
}
await tester.pump(); await tester.pump();
expect(mockHelper.invocations, containsAllInOrder(<void>[ expect(mockHelper.invocations, containsAllInOrder(<void>[
...@@ -209,13 +227,25 @@ void main() { ...@@ -209,13 +227,25 @@ void main() {
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value. refreshIndicatorExtent: 60, // default value.
), ),
matchesBuilder( if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) matchesBuilder(
refreshState: RefreshIndicatorMode.drag,
pulledExtent: moreOrLessEquals(97.3552275),
refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value.
)
else matchesBuilder(
refreshState: RefreshIndicatorMode.drag, refreshState: RefreshIndicatorMode.drag,
pulledExtent: moreOrLessEquals(86.78169), pulledExtent: moreOrLessEquals(86.78169),
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value. refreshIndicatorExtent: 60, // default value.
), ),
matchesBuilder( if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) matchesBuilder(
refreshState: RefreshIndicatorMode.armed,
pulledExtent: moreOrLessEquals(100.79409877743257),
refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value.
)
else matchesBuilder(
refreshState: RefreshIndicatorMode.armed, refreshState: RefreshIndicatorMode.armed,
pulledExtent: moreOrLessEquals(105.80452021305739), pulledExtent: moreOrLessEquals(105.80452021305739),
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
...@@ -262,7 +292,13 @@ void main() { ...@@ -262,7 +292,13 @@ void main() {
refreshIndicatorExtent: 60, // Default value. refreshIndicatorExtent: 60, // Default value.
), ),
equals(const RefreshTaskInvocation()), equals(const RefreshTaskInvocation()),
matchesBuilder( if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) matchesBuilder(
refreshState: RefreshIndicatorMode.armed,
pulledExtent: moreOrLessEquals(124.87933920045268),
refreshTriggerPullDistance: 100, // Default value.
refreshIndicatorExtent: 60, // Default value.
)
else matchesBuilder(
refreshState: RefreshIndicatorMode.armed, refreshState: RefreshIndicatorMode.armed,
pulledExtent: moreOrLessEquals(127.10396988577114), pulledExtent: moreOrLessEquals(127.10396988577114),
refreshTriggerPullDistance: 100, // Default value. refreshTriggerPullDistance: 100, // Default value.
...@@ -306,6 +342,7 @@ void main() { ...@@ -306,6 +342,7 @@ void main() {
(WidgetTester tester) async { (WidgetTester tester) async {
final FlutterError error = FlutterError('Oops'); final FlutterError error = FlutterError('Oops');
double errorCount = 0; double errorCount = 0;
final TargetPlatform? platform = debugDefaultTargetPlatformOverride; // Will not be correct within the zone.
runZonedGuarded( runZonedGuarded(
() async { () async {
...@@ -337,7 +374,13 @@ void main() { ...@@ -337,7 +374,13 @@ void main() {
refreshTriggerPullDistance: 100, // Default value. refreshTriggerPullDistance: 100, // Default value.
), ),
equals(const RefreshTaskInvocation()), equals(const RefreshTaskInvocation()),
matchesBuilder( if (platform == TargetPlatform.macOS) matchesBuilder(
refreshState: RefreshIndicatorMode.armed,
pulledExtent: moreOrLessEquals(124.87933920045268),
refreshTriggerPullDistance: 100, // Default value.
refreshIndicatorExtent: 60, // Default value.
)
else matchesBuilder(
refreshState: RefreshIndicatorMode.armed, refreshState: RefreshIndicatorMode.armed,
pulledExtent: moreOrLessEquals(127.10396988577114), pulledExtent: moreOrLessEquals(127.10396988577114),
refreshIndicatorExtent: 60, // Default value. refreshIndicatorExtent: 60, // Default value.
...@@ -415,7 +458,12 @@ void main() { ...@@ -415,7 +458,12 @@ void main() {
const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0), const Rect.fromLTRB(0.0, 0.0, 800.0, 150.0),
); );
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await tester.drag(find.text('0'), const Offset(0.0, -600.0), touchSlopY: 0, warnIfMissed: false); // hits the list
}
else {
await tester.drag(find.text('0'), const Offset(0.0, -300.0), touchSlopY: 0, warnIfMissed: false); // hits the list await tester.drag(find.text('0'), const Offset(0.0, -300.0), touchSlopY: 0, warnIfMissed: false); // hits the list
}
await tester.pump(); await tester.pump();
// Refresh indicator still being told to layout the same way. // Refresh indicator still being told to layout the same way.
...@@ -427,6 +475,21 @@ void main() { ...@@ -427,6 +475,21 @@ void main() {
))); )));
// Now the sliver is scrolled off screen. // Now the sliver is scrolled off screen.
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(
tester.getTopLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy,
moreOrLessEquals(-38.625),
);
expect(
tester.getBottomLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy,
moreOrLessEquals(21.375),
);
expect(
tester.getTopLeft(find.widgetWithText(Center, '0')).dy,
moreOrLessEquals(21.375),
);
}
else {
expect( expect(
tester.getTopLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy, tester.getTopLeft(find.widgetWithText(Center, '-1', skipOffstage: false)).dy,
moreOrLessEquals(-175.38461538461536), moreOrLessEquals(-175.38461538461536),
...@@ -439,6 +502,7 @@ void main() { ...@@ -439,6 +502,7 @@ void main() {
tester.getTopLeft(find.widgetWithText(Center, '0')).dy, tester.getTopLeft(find.widgetWithText(Center, '0')).dy,
moreOrLessEquals(-115.38461538461536), moreOrLessEquals(-115.38461538461536),
); );
}
// Scroll the top of the refresh indicator back to overscroll, it will // Scroll the top of the refresh indicator back to overscroll, it will
// snap to the size of the refresh indicator and stay there. // snap to the size of the refresh indicator and stay there.
...@@ -586,6 +650,23 @@ void main() { ...@@ -586,6 +650,23 @@ void main() {
// Waiting for refresh control to reach approximately 5% of height // Waiting for refresh control to reach approximately 5% of height
await tester.pump(const Duration(milliseconds: 400)); await tester.pump(const Duration(milliseconds: 400));
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(
tester.getRect(find.widgetWithText(Center, '0')).top,
moreOrLessEquals(3.9543032206542765, epsilon: 4e-1),
);
expect(
tester.getRect(find.widgetWithText(Center, '-1')).height,
moreOrLessEquals(3.9543032206542765, epsilon: 4e-1),
);
expect(mockHelper.invocations, contains(matchesBuilder(
refreshState: RefreshIndicatorMode.inactive,
pulledExtent: 3.9543032206542765, // ~5% of 60.0
refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value.
)));
}
else {
expect( expect(
tester.getRect(find.widgetWithText(Center, '0')).top, tester.getRect(find.widgetWithText(Center, '0')).top,
moreOrLessEquals(3.0, epsilon: 4e-1), moreOrLessEquals(3.0, epsilon: 4e-1),
...@@ -600,6 +681,7 @@ void main() { ...@@ -600,6 +681,7 @@ void main() {
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value. refreshIndicatorExtent: 60, // default value.
))); )));
}
expect(find.text('-1'), findsOneWidget); expect(find.text('-1'), findsOneWidget);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
...@@ -638,17 +720,30 @@ void main() { ...@@ -638,17 +720,30 @@ void main() {
// Let it start going away but not fully. // Let it start going away but not fully.
await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100));
// The refresh indicator is still building. // The refresh indicator is still building.
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(mockHelper.invocations, contains(matchesBuilder(
refreshState: RefreshIndicatorMode.done,
pulledExtent: 90.13497854600749,
refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value.
)));
expect(
tester.getBottomLeft(find.widgetWithText(Center, '-1')).dy,
moreOrLessEquals(90.13497854600749),
);
}
else {
expect(mockHelper.invocations, contains(matchesBuilder( expect(mockHelper.invocations, contains(matchesBuilder(
refreshState: RefreshIndicatorMode.done, refreshState: RefreshIndicatorMode.done,
pulledExtent: 91.31180913199277, pulledExtent: 91.31180913199277,
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value. refreshIndicatorExtent: 60, // default value.
))); )));
expect( expect(
tester.getBottomLeft(find.widgetWithText(Center, '-1')).dy, tester.getBottomLeft(find.widgetWithText(Center, '-1')).dy,
moreOrLessEquals(91.311809131992776), moreOrLessEquals(91.311809131992776),
); );
}
// Start another drag by an amount that would have been enough to // Start another drag by an amount that would have been enough to
// trigger another refresh if it were in the right state. // trigger another refresh if it were in the right state.
...@@ -657,12 +752,22 @@ void main() { ...@@ -657,12 +752,22 @@ void main() {
// Instead, it's still in the done state because the sliver never // Instead, it's still in the done state because the sliver never
// fully retracted. // fully retracted.
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(mockHelper.invocations, contains(matchesBuilder(
refreshState: RefreshIndicatorMode.done,
pulledExtent: 97.71721346565732,
refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value.
)));
}
else {
expect(mockHelper.invocations, contains(matchesBuilder( expect(mockHelper.invocations, contains(matchesBuilder(
refreshState: RefreshIndicatorMode.done, refreshState: RefreshIndicatorMode.done,
pulledExtent: 147.3772721631821, pulledExtent: 147.3772721631821,
refreshTriggerPullDistance: 100, // default value. refreshTriggerPullDistance: 100, // default value.
refreshIndicatorExtent: 60, // default value. refreshIndicatorExtent: 60, // default value.
))); )));
}
// Now let it fully go away. // Now let it fully go away.
await tester.pump(const Duration(seconds: 5)); await tester.pump(const Duration(seconds: 5));
...@@ -881,12 +986,22 @@ void main() { ...@@ -881,12 +986,22 @@ void main() {
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(mockHelper.invocations.last, matchesBuilder(
refreshState: RefreshIndicatorMode.done,
pulledExtent: moreOrLessEquals(148.36088180097366),
refreshTriggerPullDistance: 100.0, // Default value.
refreshIndicatorExtent: 60.0, // Default value.
));
}
else {
expect(mockHelper.invocations.last, matchesBuilder( expect(mockHelper.invocations.last, matchesBuilder(
refreshState: RefreshIndicatorMode.done, refreshState: RefreshIndicatorMode.done,
pulledExtent: moreOrLessEquals(148.6463892921364), pulledExtent: moreOrLessEquals(148.6463892921364),
refreshTriggerPullDistance: 100.0, // Default value. refreshTriggerPullDistance: 100.0, // Default value.
refreshIndicatorExtent: 60.0, // Default value. refreshIndicatorExtent: 60.0, // Default value.
)); ));
}
await tester.pump(const Duration(seconds: 5)); await tester.pump(const Duration(seconds: 5));
expect(find.text('-1'), findsNothing); expect(find.text('-1'), findsNothing);
...@@ -1038,8 +1153,12 @@ void main() { ...@@ -1038,8 +1153,12 @@ void main() {
CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
RefreshIndicatorMode.drag, RefreshIndicatorMode.drag,
); );
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await gesture.moveBy(const Offset(0.0, 20.0)); // Overscrolling, need to move more than 1px.
}
else {
await gesture.moveBy(const Offset(0.0, 3.0)); // Overscrolling, need to move more than 1px. await gesture.moveBy(const Offset(0.0, 3.0)); // Overscrolling, need to move more than 1px.
}
await tester.pump(); await tester.pump();
expect( expect(
CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
...@@ -1074,12 +1193,25 @@ void main() { ...@@ -1074,12 +1193,25 @@ void main() {
RefreshIndicatorMode.armed, RefreshIndicatorMode.armed,
); );
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await gesture.moveBy(const Offset(0.0, -310.0)); // Overscrolling, need to move more than -40.
}
else {
await gesture.moveBy(const Offset(0.0, -80.0)); // Overscrolling, need to move more than -40. await gesture.moveBy(const Offset(0.0, -80.0)); // Overscrolling, need to move more than -40.
}
await tester.pump(); await tester.pump();
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(49.469222222222214), // Below 50 now.
);
}
else {
expect( expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(49.775111111111116), // Below 50 now. moreOrLessEquals(49.775111111111116), // Below 50 now.
); );
}
expect( expect(
CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
RefreshIndicatorMode.refresh, RefreshIndicatorMode.refresh,
...@@ -1169,24 +1301,50 @@ void main() { ...@@ -1169,24 +1301,50 @@ void main() {
await tester.pump(); await tester.pump();
// Now back in overscroll mode. // Now back in overscroll mode.
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await gesture.moveBy(const Offset(0.0, -590.0));
}
else {
await gesture.moveBy(const Offset(0.0, -200.0)); await gesture.moveBy(const Offset(0.0, -200.0));
}
await tester.pump(); await tester.pump();
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(25.916444444444423),
);
}
else {
expect( expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(27.944444444444457), moreOrLessEquals(27.944444444444457),
); );
}
// Need to bring it to 100 * 0.1 to reset to inactive. // Need to bring it to 100 * 0.1 to reset to inactive.
expect( expect(
CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
RefreshIndicatorMode.done, RefreshIndicatorMode.done,
); );
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await gesture.moveBy(const Offset(0.0, -160.0));
}
else {
await gesture.moveBy(const Offset(0.0, -35.0)); await gesture.moveBy(const Offset(0.0, -35.0));
}
await tester.pump(); await tester.pump();
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(9.15133037440173),
);
}
else {
expect( expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(9.313890708161875), moreOrLessEquals(9.313890708161875),
); );
}
expect( expect(
CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))), CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder))),
RefreshIndicatorMode.inactive, RefreshIndicatorMode.inactive,
...@@ -1221,12 +1379,19 @@ void main() { ...@@ -1221,12 +1379,19 @@ void main() {
); );
await tester.pump(); // Sliver scroll offset correction is applied one frame later. await tester.pump(); // Sliver scroll offset correction is applied one frame later.
double indicatorDestinationPosition = -145.0332383665717;
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await gesture.moveBy(const Offset(0.0, -600.0));
indicatorDestinationPosition = -164.33475946989466;
}
else {
await gesture.moveBy(const Offset(0.0, -300.0)); await gesture.moveBy(const Offset(0.0, -300.0));
}
await tester.pump(); await tester.pump();
// The refresh indicator is offscreen now. // The refresh indicator is offscreen now.
expect( expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(-145.0332383665717), moreOrLessEquals(indicatorDestinationPosition),
); );
expect( expect(
CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))), CupertinoSliverRefreshControl.state(tester.element(find.byType(LayoutBuilder, skipOffstage: false))),
...@@ -1243,13 +1408,13 @@ void main() { ...@@ -1243,13 +1408,13 @@ void main() {
// Nothing moved. // Nothing moved.
expect( expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(-145.0332383665717), moreOrLessEquals(indicatorDestinationPosition),
); );
await tester.pump(const Duration(seconds: 2)); await tester.pump(const Duration(seconds: 2));
// Everything stayed as is. // Everything stayed as is.
expect( expect(
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy, tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
moreOrLessEquals(-145.0332383665717), moreOrLessEquals(indicatorDestinationPosition),
); );
}, },
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -406,12 +407,17 @@ void main() { ...@@ -406,12 +407,17 @@ void main() {
), ),
); );
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await tester.fling(find.text('A'), const Offset(0.0, 1500.0), 10000.0);
}
else {
await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0); await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
}
await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 100));
expect(lastScrollOffset = controller.offset, lessThan(0.0)); expect(lastScrollOffset = controller.offset, lessThan(0.0));
expect(refreshCalled, isFalse); expect(refreshCalled, isFalse);
await tester.pump(const Duration(milliseconds: 100)); await tester.pump(const Duration(milliseconds: 400));
expect(controller.offset, greaterThan(lastScrollOffset)); expect(controller.offset, greaterThan(lastScrollOffset));
expect(controller.offset, lessThan(0.0)); expect(controller.offset, lessThan(0.0));
expect(refreshCalled, isTrue); expect(refreshCalled, isTrue);
......
...@@ -47,4 +47,25 @@ void main() { ...@@ -47,4 +47,25 @@ void main() {
expect(friction.timeAtX(101.0), double.infinity); expect(friction.timeAtX(101.0), double.infinity);
expect(friction.timeAtX(40.0), double.infinity); expect(friction.timeAtX(40.0), double.infinity);
}); });
test('Friction simulation constant deceleration', () {
final FrictionSimulation friction = FrictionSimulation(0.135, 100.0, -100.0, constantDeceleration: 100);
expect(friction.x(0.0), moreOrLessEquals(100.0));
expect(friction.dx(0.0), moreOrLessEquals(-100.0));
expect(friction.x(0.1), moreOrLessEquals(91.0, epsilon: 1.0));
expect(friction.x(0.5), moreOrLessEquals(80.0, epsilon: 1.0));
expect(friction.x(2.0), moreOrLessEquals(80.0, epsilon: 1.0));
expect(friction.finalX, moreOrLessEquals(80.0, epsilon: 1.0));
expect(friction.timeAtX(100.0), 0.0);
expect(friction.timeAtX(friction.x(0.1)), moreOrLessEquals(0.1));
expect(friction.timeAtX(friction.x(0.2)), moreOrLessEquals(0.2));
expect(friction.timeAtX(friction.x(0.3)), moreOrLessEquals(0.3));
expect(friction.timeAtX(101.0), double.infinity);
expect(friction.timeAtX(40.0), double.infinity);
});
} }
...@@ -287,7 +287,12 @@ void main() { ...@@ -287,7 +287,12 @@ void main() {
expect(taps, 1); expect(taps, 1);
expect(find.text('Item 1'), findsNothing); expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 21'), findsNothing); expect(find.text('Item 21'), findsNothing);
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(find.text('Item 40'), findsOneWidget);
}
else {
expect(find.text('Item 70'), findsOneWidget); expect(find.text('Item 70'), findsOneWidget);
}
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgets('Can be flung down when not full height', (WidgetTester tester) async { testWidgets('Can be flung down when not full height', (WidgetTester tester) async {
...@@ -321,9 +326,14 @@ void main() { ...@@ -321,9 +326,14 @@ void main() {
expect(taps, 1); expect(taps, 1);
expect(find.text('Item 1'), findsNothing); expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 21'), findsNothing); expect(find.text('Item 21'), findsNothing);
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
expect(find.text('Item 40'), findsOneWidget);
await tester.fling(find.text('Item 40'), const Offset(0, 200), 2000);
}
else {
expect(find.text('Item 70'), findsOneWidget); expect(find.text('Item 70'), findsOneWidget);
await tester.fling(find.text('Item 70'), const Offset(0, 200), 2000); await tester.fling(find.text('Item 70'), const Offset(0, 200), 2000);
}
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('TapHere'), findsOneWidget); expect(find.text('TapHere'), findsOneWidget);
await tester.tap(find.text('TapHere'), warnIfMissed: false); await tester.tap(find.text('TapHere'), warnIfMissed: false);
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
// machines. // machines.
@Tags(<String>['reduced-test-set']) @Tags(<String>['reduced-test-set'])
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -1352,7 +1353,8 @@ void main() { ...@@ -1352,7 +1353,8 @@ void main() {
find.byType(ListWheelScrollView), find.byType(ListWheelScrollView),
// High and random numbers that's unlikely to land on exact multiples of 100. // High and random numbers that's unlikely to land on exact multiples of 100.
const Offset(0.0, -567.0), const Offset(0.0, -567.0),
678.0, // macOS has reduced ballistic distance, need to increase speed to compensate.
debugDefaultTargetPlatformOverride == TargetPlatform.macOS ? 1678.0 : 678.0,
); );
// After the drag, 40 + 567px should be on the 46th item. // After the drag, 40 + 567px should be on the 46th item.
......
...@@ -227,7 +227,12 @@ void main() { ...@@ -227,7 +227,12 @@ void main() {
expect(find.text('aaa2'), findsOneWidget); expect(find.text('aaa2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 250)); await tester.pump(const Duration(milliseconds: 250));
final Offset point1 = tester.getCenter(find.text('aaa1')); final Offset point1 = tester.getCenter(find.text('aaa1'));
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await tester.dragFrom(point1, const Offset(0.0, 400.0));
}
else {
await tester.dragFrom(point1, const Offset(0.0, 200.0)); await tester.dragFrom(point1, const Offset(0.0, 200.0));
}
await tester.pump(); await tester.pump();
expect( expect(
tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, tester.renderObject<RenderBox>(find.byType(AppBar)).size.height,
...@@ -245,7 +250,12 @@ void main() { ...@@ -245,7 +250,12 @@ void main() {
expect(find.text('aaa2'), findsOneWidget); expect(find.text('aaa2'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 250)); await tester.pump(const Duration(milliseconds: 250));
final Offset point = tester.getCenter(find.text('aaa1')); final Offset point = tester.getCenter(find.text('aaa1'));
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await tester.flingFrom(point, const Offset(0.0, 200.0), 15000.0);
}
else {
await tester.flingFrom(point, const Offset(0.0, 200.0), 5000.0); await tester.flingFrom(point, const Offset(0.0, 200.0), 5000.0);
}
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// 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';
import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -271,7 +272,12 @@ void main() { ...@@ -271,7 +272,12 @@ void main() {
expect(sizeOf(0), equals(const Size(800.0, 600.0))); expect(sizeOf(0), equals(const Size(800.0, 600.0)));
// Easing overscroll past overscroll limit. // Easing overscroll past overscroll limit.
if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS) {
await tester.drag(find.byType(PageView), const Offset(-500.0, 0.0));
}
else {
await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0)); await tester.drag(find.byType(PageView), const Offset(-200.0, 0.0));
}
await tester.pump(); await tester.pump();
expect(leftOf(0), lessThan(0.0)); expect(leftOf(0), lessThan(0.0));
......
...@@ -85,11 +85,12 @@ void main() { ...@@ -85,11 +85,12 @@ void main() {
final double macOSResult = getCurrentOffset(); final double macOSResult = getCurrentOffset();
expect(androidResult, lessThan(iOSResult)); // iOS is slipperier than Android expect(androidResult, lessThan(iOSResult)); // iOS is slipperier than Android
expect(androidResult, lessThan(macOSResult)); // macOS is slipperier than Android expect(macOSResult, lessThan(iOSResult)); // iOS is slipperier than macOS
expect(macOSResult, lessThan(androidResult)); // Android is slipperier than macOS
expect(linuxResult, lessThan(iOSResult)); // iOS is slipperier than Linux expect(linuxResult, lessThan(iOSResult)); // iOS is slipperier than Linux
expect(linuxResult, lessThan(macOSResult)); // macOS is slipperier than Linux expect(macOSResult, lessThan(linuxResult)); // Linux is slipperier than macOS
expect(windowsResult, lessThan(iOSResult)); // iOS is slipperier than Windows expect(windowsResult, lessThan(iOSResult)); // iOS is slipperier than Windows
expect(windowsResult, lessThan(macOSResult)); // macOS is slipperier than Windows expect(macOSResult, lessThan(windowsResult)); // Windows is slipperier than macOS
expect(windowsResult, equals(androidResult)); expect(windowsResult, equals(androidResult));
expect(windowsResult, equals(androidResult)); expect(windowsResult, equals(androidResult));
expect(linuxResult, equals(androidResult)); expect(linuxResult, equals(androidResult));
......
...@@ -134,8 +134,9 @@ void main() { ...@@ -134,8 +134,9 @@ void main() {
await tester.pump(const Duration(seconds: 5)); await tester.pump(const Duration(seconds: 5));
final double macOSResult = getScrollOffset(tester); final double macOSResult = getScrollOffset(tester);
expect(macOSResult, lessThan(androidResult)); // macOS is slipperier than Android
expect(androidResult, lessThan(iOSResult)); // iOS is slipperier than Android expect(androidResult, lessThan(iOSResult)); // iOS is slipperier than Android
expect(androidResult, lessThan(macOSResult)); // macOS is slipperier than Android expect(macOSResult, lessThan(iOSResult)); // iOS is slipperier than macOS
}); });
testWidgets('Holding scroll', (WidgetTester tester) async { testWidgets('Holding scroll', (WidgetTester tester) async {
......
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