Make ClampingScrollSimulation ballistic and more like Android (#120420)
Make ClampingScrollSimulation ballistic and more like Android
This commit is contained in:
parent
ecd7518df5
commit
e13677450c
@ -123,98 +123,129 @@ class BouncingScrollSimulation extends Simulation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An implementation of scroll physics that matches Android.
|
/// An implementation of scroll physics that aligns with Android.
|
||||||
|
///
|
||||||
|
/// For any value of [velocity], this travels the same total distance as the
|
||||||
|
/// Android scroll physics.
|
||||||
|
///
|
||||||
|
/// This scroll physics has been adjusted relative to Android's in order to make
|
||||||
|
/// it ballistic, meaning that the deceleration at any moment is a function only
|
||||||
|
/// of the current velocity [dx] and does not depend on how long ago the
|
||||||
|
/// simulation was started. (This is required by Flutter's scrolling protocol,
|
||||||
|
/// where [ScrollActivityDelegate.goBallistic] may restart a scroll activity
|
||||||
|
/// using only its current velocity and the scroll position's own state.)
|
||||||
|
/// Compared to this scroll physics, Android's moves faster at the very
|
||||||
|
/// beginning, then slower, and it ends at the same place but a little later.
|
||||||
|
///
|
||||||
|
/// Times are measured in seconds, and positions in logical pixels.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
|
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
|
||||||
//
|
//
|
||||||
// This class is based on Scroller.java from Android:
|
// This class is based on OverScroller.java from Android:
|
||||||
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
|
// https://android.googlesource.com/platform/frameworks/base/+/android-13.0.0_r24/core/java/android/widget/OverScroller.java#738
|
||||||
|
// and in particular class SplineOverScroller (at the end of the file), starting
|
||||||
|
// at method "fling". (A very similar algorithm is in Scroller.java in the same
|
||||||
|
// directory, but OverScroller is what's used by RecyclerView.)
|
||||||
//
|
//
|
||||||
// The "See..." comments below refer to Scroller methods and values. Some
|
// In the Android implementation, times are in milliseconds, positions are in
|
||||||
// simplifications have been made.
|
// physical pixels, but velocity is in physical pixels per whole second.
|
||||||
|
//
|
||||||
|
// The "See..." comments below refer to SplineOverScroller methods and values.
|
||||||
class ClampingScrollSimulation extends Simulation {
|
class ClampingScrollSimulation extends Simulation {
|
||||||
/// Creates a scroll physics simulation that matches Android scrolling.
|
/// Creates a scroll physics simulation that aligns with Android scrolling.
|
||||||
ClampingScrollSimulation({
|
ClampingScrollSimulation({
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.velocity,
|
required this.velocity,
|
||||||
this.friction = 0.015,
|
this.friction = 0.015,
|
||||||
super.tolerance,
|
super.tolerance,
|
||||||
}) : assert(_flingVelocityPenetration(0.0) == _initialVelocityPenetration) {
|
}) {
|
||||||
_duration = _flingDuration(velocity);
|
_duration = _flingDuration();
|
||||||
_distance = (velocity * _duration / _initialVelocityPenetration).abs();
|
_distance = _flingDistance();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The position of the particle at the beginning of the simulation.
|
/// The position of the particle at the beginning of the simulation, in
|
||||||
|
/// logical pixels.
|
||||||
final double position;
|
final double position;
|
||||||
|
|
||||||
/// The velocity at which the particle is traveling at the beginning of the
|
/// The velocity at which the particle is traveling at the beginning of the
|
||||||
/// simulation.
|
/// simulation, in logical pixels per second.
|
||||||
final double velocity;
|
final double velocity;
|
||||||
|
|
||||||
/// The amount of friction the particle experiences as it travels.
|
/// The amount of friction the particle experiences as it travels.
|
||||||
///
|
///
|
||||||
/// The more friction the particle experiences, the sooner it stops.
|
/// The more friction the particle experiences, the sooner it stops and the
|
||||||
|
/// less far it travels.
|
||||||
|
///
|
||||||
|
/// The default value causes the particle to travel the same total distance
|
||||||
|
/// as in the Android scroll physics.
|
||||||
|
// See mFlingFriction.
|
||||||
final double friction;
|
final double friction;
|
||||||
|
|
||||||
|
/// The total time the simulation will run, in seconds.
|
||||||
late double _duration;
|
late double _duration;
|
||||||
|
|
||||||
|
/// The total, signed, distance the simulation will travel, in logical pixels.
|
||||||
late double _distance;
|
late double _distance;
|
||||||
|
|
||||||
// See DECELERATION_RATE.
|
// See DECELERATION_RATE.
|
||||||
static final double _kDecelerationRate = math.log(0.78) / math.log(0.9);
|
static final double _kDecelerationRate = math.log(0.78) / math.log(0.9);
|
||||||
|
|
||||||
// See computeDeceleration().
|
// See INFLEXION.
|
||||||
static double _decelerationForFriction(double friction) {
|
static const double _kInflexion = 0.35;
|
||||||
return friction * 61774.04968;
|
|
||||||
|
// See mPhysicalCoeff. This has a value of 0.84 times Earth gravity,
|
||||||
|
// expressed in units of logical pixels per second^2.
|
||||||
|
static const double _physicalCoeff =
|
||||||
|
9.80665 // g, in meters per second^2
|
||||||
|
* 39.37 // 1 meter / 1 inch
|
||||||
|
* 160.0 // 1 inch / 1 logical pixel
|
||||||
|
* 0.84; // "look and feel tuning"
|
||||||
|
|
||||||
|
// See getSplineFlingDuration().
|
||||||
|
double _flingDuration() {
|
||||||
|
// See getSplineDeceleration(). That function's value is
|
||||||
|
// math.log(velocity.abs() / referenceVelocity).
|
||||||
|
final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
|
||||||
|
|
||||||
|
// This is the value getSplineFlingDuration() would return, but in seconds.
|
||||||
|
final double androidDuration =
|
||||||
|
math.pow(velocity.abs() / referenceVelocity,
|
||||||
|
1 / (_kDecelerationRate - 1.0)) as double;
|
||||||
|
|
||||||
|
// We finish a bit sooner than Android, in order to travel the
|
||||||
|
// same total distance.
|
||||||
|
return _kDecelerationRate * _kInflexion * androidDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See getSplineFlingDuration(). Returns a value in seconds.
|
// See getSplineFlingDistance(). This returns the same value but with the
|
||||||
double _flingDuration(double velocity) {
|
// sign of [velocity], and in logical pixels.
|
||||||
// See mPhysicalCoeff
|
double _flingDistance() {
|
||||||
final double scaledFriction = friction * _decelerationForFriction(0.84);
|
final double distance = velocity * _duration / _kDecelerationRate;
|
||||||
|
assert(() {
|
||||||
// See getSplineDeceleration().
|
// This is the more complicated calculation that getSplineFlingDistance()
|
||||||
final double deceleration = math.log(0.35 * velocity.abs() / scaledFriction);
|
// actually performs, which boils down to the much simpler formula above.
|
||||||
|
final double referenceVelocity = friction * _physicalCoeff / _kInflexion;
|
||||||
return math.exp(deceleration / (_kDecelerationRate - 1.0));
|
final double logVelocity = math.log(velocity.abs() / referenceVelocity);
|
||||||
}
|
final double distanceAgain =
|
||||||
|
friction * _physicalCoeff
|
||||||
// Based on a cubic curve fit to the Scroller.computeScrollOffset() values
|
* math.exp(logVelocity * _kDecelerationRate / (_kDecelerationRate - 1.0));
|
||||||
// produced for an initial velocity of 4000. The value of Scroller.getDuration()
|
return (distance.abs() - distanceAgain).abs() < tolerance.distance;
|
||||||
// and Scroller.getFinalY() were 686ms and 961 pixels respectively.
|
}());
|
||||||
//
|
return distance;
|
||||||
// Algebra courtesy of Wolfram Alpha.
|
|
||||||
//
|
|
||||||
// f(x) = scrollOffset, x is time in milliseconds
|
|
||||||
// f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x - 3.15307
|
|
||||||
// f(x) = 3.60882×10^-6 x^3 - 0.00668009 x^2 + 4.29427 x, so f(0) is 0
|
|
||||||
// f(686ms) = 961 pixels
|
|
||||||
// Scale to f(0 <= t <= 1.0), x = t * 686
|
|
||||||
// f(t) = 1165.03 t^3 - 3143.62 t^2 + 2945.87 t
|
|
||||||
// Scale f(t) so that 0.0 <= f(t) <= 1.0
|
|
||||||
// f(t) = (1165.03 t^3 - 3143.62 t^2 + 2945.87 t) / 961.0
|
|
||||||
// = 1.2 t^3 - 3.27 t^2 + 3.065 t
|
|
||||||
static const double _initialVelocityPenetration = 3.065;
|
|
||||||
static double _flingDistancePenetration(double t) {
|
|
||||||
return (1.2 * t * t * t) - (3.27 * t * t) + (_initialVelocityPenetration * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The derivative of the _flingDistancePenetration() function.
|
|
||||||
static double _flingVelocityPenetration(double t) {
|
|
||||||
return (3.6 * t * t) - (6.54 * t) + _initialVelocityPenetration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double x(double time) {
|
double x(double time) {
|
||||||
final double t = clampDouble(time / _duration, 0.0, 1.0);
|
final double t = clampDouble(time / _duration, 0.0, 1.0);
|
||||||
return position + _distance * _flingDistancePenetration(t) * velocity.sign;
|
return position + _distance * (1.0 - math.pow(1.0 - t, _kDecelerationRate));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double dx(double time) {
|
double dx(double time) {
|
||||||
final double t = clampDouble(time / _duration, 0.0, 1.0);
|
final double t = clampDouble(time / _duration, 0.0, 1.0);
|
||||||
return _distance * _flingVelocityPenetration(t) * velocity.sign / _duration;
|
return velocity * math.pow(1.0 - t, _kDecelerationRate - 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// 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 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
@ -23,4 +25,134 @@ void main() {
|
|||||||
checkInitialConditions(75.0, 614.2093);
|
checkInitialConditions(75.0, 614.2093);
|
||||||
checkInitialConditions(5469.0, 182.114534);
|
checkInitialConditions(5469.0, 182.114534);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ClampingScrollSimulation only decelerates, never speeds up', () {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/113424
|
||||||
|
final ClampingScrollSimulation simulation =
|
||||||
|
ClampingScrollSimulation(position: 0, velocity: 8000.0);
|
||||||
|
double time = 0.0;
|
||||||
|
double velocity = simulation.dx(time);
|
||||||
|
while (!simulation.isDone(time)) {
|
||||||
|
expect(time, lessThan(3.0));
|
||||||
|
time += 1 / 60;
|
||||||
|
final double nextVelocity = simulation.dx(time);
|
||||||
|
expect(nextVelocity, lessThanOrEqualTo(velocity));
|
||||||
|
velocity = nextVelocity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ClampingScrollSimulation reaches a smooth stop: velocity is continuous and goes to zero', () {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/113424
|
||||||
|
const double initialVelocity = 8000.0;
|
||||||
|
const double maxDeceleration = 5130.0; // -acceleration(initialVelocity), from formula below
|
||||||
|
final ClampingScrollSimulation simulation =
|
||||||
|
ClampingScrollSimulation(position: 0, velocity: initialVelocity);
|
||||||
|
|
||||||
|
double time = 0.0;
|
||||||
|
double velocity = simulation.dx(time);
|
||||||
|
const double delta = 1 / 60;
|
||||||
|
do {
|
||||||
|
expect(time, lessThan(3.0));
|
||||||
|
time += delta;
|
||||||
|
final double nextVelocity = simulation.dx(time);
|
||||||
|
expect((nextVelocity - velocity).abs(), lessThan(delta * maxDeceleration));
|
||||||
|
velocity = nextVelocity;
|
||||||
|
} while (!simulation.isDone(time));
|
||||||
|
expect(velocity, moreOrLessEquals(0.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ClampingScrollSimulation is ballistic', () {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/120338
|
||||||
|
const double delta = 1 / 90;
|
||||||
|
final ClampingScrollSimulation undisturbed =
|
||||||
|
ClampingScrollSimulation(position: 0, velocity: 8000.0);
|
||||||
|
|
||||||
|
double time = 0.0;
|
||||||
|
ClampingScrollSimulation restarted = undisturbed;
|
||||||
|
final List<double> xsRestarted = <double>[];
|
||||||
|
final List<double> xsUndisturbed = <double>[];
|
||||||
|
final List<double> dxsRestarted = <double>[];
|
||||||
|
final List<double> dxsUndisturbed = <double>[];
|
||||||
|
do {
|
||||||
|
expect(time, lessThan(4.0));
|
||||||
|
time += delta;
|
||||||
|
restarted = ClampingScrollSimulation(
|
||||||
|
position: restarted.x(delta), velocity: restarted.dx(delta));
|
||||||
|
xsRestarted.add(restarted.x(0));
|
||||||
|
xsUndisturbed.add(undisturbed.x(time));
|
||||||
|
dxsRestarted.add(restarted.dx(0));
|
||||||
|
dxsUndisturbed.add(undisturbed.dx(time));
|
||||||
|
} while (!restarted.isDone(0) || !undisturbed.isDone(time));
|
||||||
|
|
||||||
|
// Compare the headline number first: the total distances traveled.
|
||||||
|
// This way, if the test fails, it shows the big final difference
|
||||||
|
// instead of the tiny difference that's in the very first frame.
|
||||||
|
expect(xsRestarted.last, moreOrLessEquals(xsUndisturbed.last));
|
||||||
|
|
||||||
|
// The whole trajectories along the way should match too.
|
||||||
|
for (int i = 0; i < xsRestarted.length; i++) {
|
||||||
|
expect(xsRestarted[i], moreOrLessEquals(xsUndisturbed[i]));
|
||||||
|
expect(dxsRestarted[i], moreOrLessEquals(dxsUndisturbed[i]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ClampingScrollSimulation satisfies a physical acceleration formula', () {
|
||||||
|
// Different regression test for https://github.com/flutter/flutter/issues/120338
|
||||||
|
//
|
||||||
|
// This one provides a formula for the particle's acceleration as a function
|
||||||
|
// of its velocity, and checks that it behaves according to that formula.
|
||||||
|
// The point isn't that it's this specific formula, but just that there's
|
||||||
|
// some formula which depends only on velocity, not time, so that the
|
||||||
|
// physical metaphor makes sense.
|
||||||
|
|
||||||
|
// Copied from the implementation.
|
||||||
|
final double kDecelerationRate = math.log(0.78) / math.log(0.9);
|
||||||
|
|
||||||
|
// Same as the referenceVelocity in _flingDuration.
|
||||||
|
const double referenceVelocity = .015 * 9.80665 * 39.37 * 160.0 * 0.84 / 0.35;
|
||||||
|
|
||||||
|
// The value of _duration when velocity == referenceVelocity.
|
||||||
|
final double referenceDuration = kDecelerationRate * 0.35;
|
||||||
|
|
||||||
|
// The rate of deceleration when dx(time) == referenceVelocity.
|
||||||
|
final double referenceDeceleration = (kDecelerationRate - 1) * referenceVelocity / referenceDuration;
|
||||||
|
|
||||||
|
double acceleration(double velocity) {
|
||||||
|
return - velocity.sign
|
||||||
|
* referenceDeceleration *
|
||||||
|
math.pow(velocity.abs() / referenceVelocity,
|
||||||
|
(kDecelerationRate - 2) / (kDecelerationRate - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
double jerk(double velocity) {
|
||||||
|
return referenceVelocity / referenceDuration / referenceDuration
|
||||||
|
* (kDecelerationRate - 1) * (kDecelerationRate - 2)
|
||||||
|
* math.pow(velocity.abs() / referenceVelocity,
|
||||||
|
(kDecelerationRate - 3) / (kDecelerationRate - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkAcceleration(double position, double velocity) {
|
||||||
|
final ClampingScrollSimulation simulation =
|
||||||
|
ClampingScrollSimulation(position: position, velocity: velocity);
|
||||||
|
double time = 0.0;
|
||||||
|
const double delta = 1/60;
|
||||||
|
for (; time < 2.0; time += delta) {
|
||||||
|
final double difference = simulation.dx(time + delta) - simulation.dx(time);
|
||||||
|
final double predictedDifference = delta * acceleration(simulation.dx(time + delta/2));
|
||||||
|
final double maxThirdDerivative = jerk(simulation.dx(time + delta));
|
||||||
|
expect((difference - predictedDifference).abs(),
|
||||||
|
lessThan(maxThirdDerivative * math.pow(delta, 2)/2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAcceleration(51.0, 2866.91537);
|
||||||
|
checkAcceleration(584.0, 2617.294734);
|
||||||
|
checkAcceleration(345.0, 1982.785934);
|
||||||
|
checkAcceleration(0.0, 1831.366634);
|
||||||
|
checkAcceleration(-156.2, 1541.57665);
|
||||||
|
checkAcceleration(4.0, 1139.250439);
|
||||||
|
checkAcceleration(4534.0, 1073.553798);
|
||||||
|
checkAcceleration(75.0, 614.2093);
|
||||||
|
checkAcceleration(5469.0, 182.114534);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,8 @@ void main() {
|
|||||||
// Regression test for https://github.com/flutter/flutter/issues/83632
|
// Regression test for https://github.com/flutter/flutter/issues/83632
|
||||||
// Before changing these values, ensure the fling results in a distance that
|
// Before changing these values, ensure the fling results in a distance that
|
||||||
// makes sense. See issue for more context.
|
// makes sense. See issue for more context.
|
||||||
expect(androidResult, greaterThan(394.0));
|
expect(androidResult, greaterThan(408.0));
|
||||||
expect(androidResult, lessThan(395.0));
|
expect(androidResult, lessThan(409.0));
|
||||||
|
|
||||||
await pumpTest(tester, TargetPlatform.linux);
|
await pumpTest(tester, TargetPlatform.linux);
|
||||||
await tester.fling(find.byType(ListView), const Offset(0.0, -dragOffset), 1000.0);
|
await tester.fling(find.byType(ListView), const Offset(0.0, -dragOffset), 1000.0);
|
||||||
@ -153,6 +153,6 @@ void main() {
|
|||||||
expect(log, equals(<String>['tap 21']));
|
expect(log, equals(<String>['tap 21']));
|
||||||
await tester.tap(find.byType(Scrollable));
|
await tester.tap(find.byType(Scrollable));
|
||||||
await tester.pump(const Duration(milliseconds: 50));
|
await tester.pump(const Duration(milliseconds: 50));
|
||||||
expect(log, equals(<String>['tap 21', 'tap 48']));
|
expect(log, equals(<String>['tap 21', 'tap 49']));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -231,7 +231,7 @@ void main() {
|
|||||||
|
|
||||||
expect(semantics, includesNodeWith(
|
expect(semantics, includesNodeWith(
|
||||||
scrollExtentMin: 0.0,
|
scrollExtentMin: 0.0,
|
||||||
scrollPosition: 380.2,
|
scrollPosition: 394.3,
|
||||||
scrollExtentMax: 520.0,
|
scrollExtentMax: 520.0,
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.scrollUp,
|
SemanticsAction.scrollUp,
|
||||||
@ -280,7 +280,7 @@ void main() {
|
|||||||
|
|
||||||
expect(semantics, includesNodeWith(
|
expect(semantics, includesNodeWith(
|
||||||
scrollExtentMin: 0.0,
|
scrollExtentMin: 0.0,
|
||||||
scrollPosition: 380.2,
|
scrollPosition: 394.3,
|
||||||
scrollExtentMax: double.infinity,
|
scrollExtentMax: double.infinity,
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.scrollUp,
|
SemanticsAction.scrollUp,
|
||||||
@ -292,7 +292,7 @@ void main() {
|
|||||||
|
|
||||||
expect(semantics, includesNodeWith(
|
expect(semantics, includesNodeWith(
|
||||||
scrollExtentMin: 0.0,
|
scrollExtentMin: 0.0,
|
||||||
scrollPosition: 760.4,
|
scrollPosition: 788.6,
|
||||||
scrollExtentMax: double.infinity,
|
scrollExtentMax: double.infinity,
|
||||||
actions: <SemanticsAction>[
|
actions: <SemanticsAction>[
|
||||||
SemanticsAction.scrollUp,
|
SemanticsAction.scrollUp,
|
||||||
|
@ -1069,8 +1069,8 @@ void main() {
|
|||||||
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
|
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
|
||||||
expect(find.byKey(const ValueKey<String>('Box 52')), findsOneWidget);
|
expect(find.byKey(const ValueKey<String>('Box 52')), findsOneWidget);
|
||||||
|
|
||||||
expect(expensiveWidgets, 38);
|
expect(expensiveWidgets, 40);
|
||||||
expect(cheapWidgets, 20);
|
expect(cheapWidgets, 21);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Can recommendDeferredLoadingForContext - override heuristic', (WidgetTester tester) async {
|
testWidgets('Can recommendDeferredLoadingForContext - override heuristic', (WidgetTester tester) async {
|
||||||
@ -1112,9 +1112,9 @@ void main() {
|
|||||||
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
|
expect(find.byKey(const ValueKey<String>('Box 0')), findsNothing);
|
||||||
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
|
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
|
||||||
|
|
||||||
expect(expensiveWidgets, 18);
|
expect(expensiveWidgets, 17);
|
||||||
expect(cheapWidgets, 40);
|
expect(cheapWidgets, 44);
|
||||||
expect(physics.count, 40 + 18);
|
expect(physics.count, 44 + 17);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Can recommendDeferredLoadingForContext - override heuristic and always return true', (WidgetTester tester) async {
|
testWidgets('Can recommendDeferredLoadingForContext - override heuristic and always return true', (WidgetTester tester) async {
|
||||||
@ -1155,7 +1155,7 @@ void main() {
|
|||||||
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
|
expect(find.byKey(const ValueKey<String>('Cheap box 52')), findsOneWidget);
|
||||||
|
|
||||||
expect(expensiveWidgets, 0);
|
expect(expensiveWidgets, 0);
|
||||||
expect(cheapWidgets, 58);
|
expect(cheapWidgets, 61);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('ensureVisible does not move PageViews', (WidgetTester tester) async {
|
testWidgets('ensureVisible does not move PageViews', (WidgetTester tester) async {
|
||||||
@ -1641,9 +1641,9 @@ void main() {
|
|||||||
await tester.sendEventToBinding(testPointer.hover(tester.getCenter(find.byType(Scrollable))));
|
await tester.sendEventToBinding(testPointer.hover(tester.getCenter(find.byType(Scrollable))));
|
||||||
await tester.sendEventToBinding(testPointer.scrollInertiaCancel()); // Cancel partway through.
|
await tester.sendEventToBinding(testPointer.scrollInertiaCancel()); // Cancel partway through.
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(getScrollOffset(tester), closeTo(333.2944, 0.0001));
|
expect(getScrollOffset(tester), closeTo(344.0642, 0.0001));
|
||||||
await tester.pump(const Duration(milliseconds: 4800));
|
await tester.pump(const Duration(milliseconds: 4800));
|
||||||
expect(getScrollOffset(tester), closeTo(333.2944, 0.0001));
|
expect(getScrollOffset(tester), closeTo(344.0642, 0.0001));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Swapping viewports in a scrollable does not crash', (WidgetTester tester) async {
|
testWidgets('Swapping viewports in a scrollable does not crash', (WidgetTester tester) async {
|
||||||
|
@ -173,7 +173,7 @@ void main() {
|
|||||||
TestSemantics(
|
TestSemantics(
|
||||||
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
|
actions: <SemanticsAction>[SemanticsAction.scrollUp, SemanticsAction.scrollDown],
|
||||||
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
|
||||||
scrollIndex: 10,
|
scrollIndex: 11,
|
||||||
children: <TestSemantics>[
|
children: <TestSemantics>[
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
label: 'Tile 7',
|
label: 'Tile 7',
|
||||||
@ -193,6 +193,7 @@ void main() {
|
|||||||
TestSemantics(
|
TestSemantics(
|
||||||
label: 'Tile 10',
|
label: 'Tile 10',
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
|
||||||
),
|
),
|
||||||
TestSemantics(
|
TestSemantics(
|
||||||
label: 'Tile 11',
|
label: 'Tile 11',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user