Skip to content

Commit daef082

Browse files
authored
PageView scroll physics to match Android (#95423)
1 parent 1feaa6c commit daef082

6 files changed

Lines changed: 411 additions & 12 deletions

File tree

‎packages/flutter/lib/src/widgets/page_view.dart‎

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'scroll_notification.dart';
2121
import 'scroll_physics.dart';
2222
import 'scroll_position.dart';
2323
import 'scroll_position_with_single_context.dart';
24+
import 'scroll_simulation.dart';
2425
import 'scroll_view.dart';
2526
import 'scrollable.dart';
2627
import 'sliver.dart';
@@ -524,10 +525,24 @@ class _ForceImplicitScrollPhysics extends ScrollPhysics {
524525
/// * [ScrollPhysics], the base class which defines the API for scrolling
525526
/// physics.
526527
/// * [PageView.physics], which can override the physics used by a page view.
528+
/// * [PageScrollSimulation], which implements Android page view scroll physics, and
529+
/// used by this class.
527530
class PageScrollPhysics extends ScrollPhysics {
528531
/// Creates physics for a [PageView].
529532
const PageScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);
530533

534+
// See Android ViewPager constants
535+
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java;l=116;drc=1dcb8847e7aca80ee78c5d9864329b93dd276379
536+
static const int _kMaxSettleDuration = 600;
537+
static const double _kMinFlingDistance = 25.0;
538+
static const double _kMinFlingVelocity = 400.0;
539+
540+
@override
541+
double get minFlingDistance => _kMinFlingDistance;
542+
543+
@override
544+
double get minFlingVelocity => _kMinFlingVelocity;
545+
531546
@override
532547
PageScrollPhysics applyTo(ScrollPhysics? ancestor) {
533548
return PageScrollPhysics(parent: buildParent(ancestor));
@@ -545,13 +560,24 @@ class PageScrollPhysics extends ScrollPhysics {
545560
return page * position.viewportDimension;
546561
}
547562

548-
double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
549-
double page = _getPage(position);
563+
double _getTargetPage(double page, Tolerance tolerance, double velocity) {
550564
if (velocity < -tolerance.velocity)
551565
page -= 0.5;
552566
else if (velocity > tolerance.velocity)
553567
page += 0.5;
554-
return _getPixels(position, page.roundToDouble());
568+
return page.roundToDouble();
569+
}
570+
571+
double _getPageDelta(ScrollMetrics position, Tolerance tolerance, double velocity) {
572+
final double page = _getPage(position);
573+
final double targetPage = _getTargetPage(page, tolerance, velocity);
574+
return targetPage - page;
575+
}
576+
577+
double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
578+
final double page = _getPage(position);
579+
final double targetPage = _getTargetPage(page, tolerance, velocity);
580+
return _getPixels(position, targetPage);
555581
}
556582

557583
@override
@@ -563,11 +589,54 @@ class PageScrollPhysics extends ScrollPhysics {
563589
return super.createBallisticSimulation(position, velocity);
564590
final Tolerance tolerance = this.tolerance;
565591
final double target = _getTargetPixels(position, tolerance, velocity);
566-
if (target != position.pixels)
567-
return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
592+
if (target != position.pixels) {
593+
// See Android ViewPager smoothScrollTo logic
594+
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java;l=952;drc=1dcb8847e7aca80ee78c5d9864329b93dd276379
595+
final double delta = target - position.pixels;
596+
final double width = position.viewportDimension;
597+
final double halfWidth = width / 2;
598+
final double distanceRatio = math.min(1.0, 1.0 * delta.abs() / width);
599+
final double distance = halfWidth + halfWidth * _distanceInfluenceForSnapDuration(distanceRatio);
600+
int duration;
601+
if (velocity.abs() > 0) {
602+
duration = 4 * (1000 * (distance / velocity).abs()).round();
603+
} else {
604+
final double pageDelta = _getPageDelta(position, tolerance, velocity).abs();
605+
// A slightly different algorithm than on Android, because
606+
// Flutter has different velocity estimate, which is more likely to
607+
// return zero pointer velocity when user holds his finger after a fling,
608+
// compared to Android's VelocityTracker.
609+
//
610+
// It was not clear why exactly this happens, since the estimate logic is
611+
// the same as on Android, so it was decided to adjust this formula
612+
// to produce similar results.
613+
//
614+
// On Android it looks like this:
615+
// duration = ((pageDelta + 1) * 100).toInt();
616+
duration = ((pageDelta * 100 + 1) * 100).toInt();
617+
}
618+
duration = math.min(duration, _kMaxSettleDuration);
619+
return PageScrollSimulation(
620+
position: position.pixels,
621+
target: target,
622+
duration: duration / 1000,
623+
);
624+
}
568625
return null;
569626
}
570627

628+
// See Android ViewPager distanceInfluenceForSnapDuration.
629+
//
630+
// We want the duration of the page snap animation to be influenced by the distance that
631+
// the screen has to travel, however, we don't want this duration to be effected in a
632+
// purely linear fashion. Instead, we use this method to moderate the effect that the distance
633+
// of travel has on the overall snap duration.
634+
double _distanceInfluenceForSnapDuration(double value) {
635+
value -= 0.5; // center the values about 0.
636+
value *= 0.3 * math.pi / 2.0;
637+
return math.sin(value);
638+
}
639+
571640
@override
572641
bool get allowImplicitScrolling => false;
573642
}

‎packages/flutter/lib/src/widgets/scroll_physics.dart‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,9 @@ class ScrollPhysics {
349349
ratio: 1.1,
350350
);
351351

352-
/// The spring to use for ballistic simulations.
352+
/// The spring which [createBallisticSimulation] should use to create
353+
/// a [SpringSimulation] to correct the scroll position when it goes
354+
/// out of range.
353355
SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
354356

355357
/// The default accuracy to which scrolling is computed.

‎packages/flutter/lib/src/widgets/scroll_simulation.dart‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:flutter/physics.dart';
1212
/// See also:
1313
///
1414
/// * [ClampingScrollSimulation], which implements Android scroll physics.
15+
/// * [PageScrollSimulation], which implements Android page view scroll physics.
1516
class BouncingScrollSimulation extends Simulation {
1617
/// Creates a simulation group for scrolling on iOS, with the given
1718
/// parameters.
@@ -134,6 +135,7 @@ class BouncingScrollSimulation extends Simulation {
134135
/// See also:
135136
///
136137
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
138+
/// * [PageScrollSimulation], which implements Android page view scroll physics.
137139
//
138140
// This class is based on Scroller.java from Android:
139141
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
@@ -229,3 +231,70 @@ class ClampingScrollSimulation extends Simulation {
229231
return time >= _duration;
230232
}
231233
}
234+
235+
/// An implementation of scroll physics that matches Android page view.
236+
///
237+
/// See also:
238+
///
239+
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
240+
/// * [ClampingScrollSimulation], which implements Android scroll physics.
241+
//
242+
// This class ports Android logic from Scroller.java `startScroll`, applying
243+
// the interpolator set in ViewPager.java.
244+
//
245+
// See Android Scroller.java `startScroll` source code
246+
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Scroller.java;l=393;drc=ae5bcf23b5f0875e455790d6af387184dbd009c1
247+
class PageScrollSimulation extends Simulation {
248+
/// Creates a scroll physics simulation that matches Android page view.
249+
PageScrollSimulation({
250+
required this.position,
251+
required this.target,
252+
required this.duration,
253+
}) : assert(position != target),
254+
_delta = target - position,
255+
_durationReciprocal = 1.0 / duration;
256+
257+
/// The position at the beginning of the simulation.
258+
final double position;
259+
260+
/// The target, which will be reached at the end of the simulation.
261+
final double target;
262+
263+
/// The duration of the simulation in seconds.
264+
final double duration;
265+
266+
final double _delta;
267+
final double _durationReciprocal;
268+
269+
/// See Android ViewPager interpolator
270+
/// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java;l=152;drc=1dcb8847e7aca80ee78c5d9864329b93dd276379
271+
double _interpolate(double t) {
272+
// y = (t - 1)^5 + 1.0
273+
t -= 1.0;
274+
return t * t * t * t * t + 1.0;
275+
}
276+
277+
/// A derivative from [_interpolate] function.
278+
double _interpolateDx(double t) {
279+
// y = 5 * (t - 1)^4
280+
t -= 1.0;
281+
return 5.0 * t * t * t * t;
282+
}
283+
284+
@override
285+
double x(double time) {
286+
time = time.clamp(0.0, duration);
287+
return position + _delta * _interpolate(time * _durationReciprocal);
288+
}
289+
290+
@override
291+
double dx(double time) {
292+
time = time.clamp(0.0, duration);
293+
return _delta * _interpolateDx(time * _durationReciprocal);
294+
}
295+
296+
@override
297+
bool isDone(double time) {
298+
return time >= duration;
299+
}
300+
}

0 commit comments

Comments
 (0)