Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions packages/flutter/lib/src/widgets/page_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
import 'scroll_simulation.dart';
import 'scroll_view.dart';
import 'scrollable.dart';
import 'sliver.dart';
Expand Down Expand Up @@ -524,10 +525,24 @@ class _ForceImplicitScrollPhysics extends ScrollPhysics {
/// * [ScrollPhysics], the base class which defines the API for scrolling
/// physics.
/// * [PageView.physics], which can override the physics used by a page view.
/// * [PageScrollSimulation], which implements Android page view scroll physics, and
/// used by this class.
class PageScrollPhysics extends ScrollPhysics {
/// Creates physics for a [PageView].
const PageScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);

// See Android ViewPager constants
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java;l=116;drc=1dcb8847e7aca80ee78c5d9864329b93dd276379
static const int _kMaxSettleDuration = 600;
static const double _kMinFlingDistance = 25.0;
static const double _kMinFlingVelocity = 400.0;

@override
double get minFlingDistance => _kMinFlingDistance;

@override
double get minFlingVelocity => _kMinFlingVelocity;

@override
PageScrollPhysics applyTo(ScrollPhysics? ancestor) {
return PageScrollPhysics(parent: buildParent(ancestor));
Expand All @@ -545,13 +560,24 @@ class PageScrollPhysics extends ScrollPhysics {
return page * position.viewportDimension;
}

double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
double page = _getPage(position);
double _getTargetPage(double page, Tolerance tolerance, double velocity) {
if (velocity < -tolerance.velocity)
page -= 0.5;
else if (velocity > tolerance.velocity)
page += 0.5;
return _getPixels(position, page.roundToDouble());
return page.roundToDouble();
}

double _getPageDelta(ScrollMetrics position, Tolerance tolerance, double velocity) {
final double page = _getPage(position);
final double targetPage = _getTargetPage(page, tolerance, velocity);
return targetPage - page;
}

double _getTargetPixels(ScrollMetrics position, Tolerance tolerance, double velocity) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Piinks I just thought whether would it make sense to make some of these methods public, so people can restore the old implementation if they want (or just change it to whatever else)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if that would be super beneficial, ultimately the changes you are introducing are the way it should be, there are just a lot of tests out there that expect the old behavior.

final double page = _getPage(position);
final double targetPage = _getTargetPage(page, tolerance, velocity);
return _getPixels(position, targetPage);
}

@override
Expand All @@ -563,11 +589,54 @@ class PageScrollPhysics extends ScrollPhysics {
return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels)
return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
if (target != position.pixels) {
// See Android ViewPager smoothScrollTo logic
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java;l=952;drc=1dcb8847e7aca80ee78c5d9864329b93dd276379
final double delta = target - position.pixels;
final double width = position.viewportDimension;
final double halfWidth = width / 2;
final double distanceRatio = math.min(1.0, 1.0 * delta.abs() / width);
final double distance = halfWidth + halfWidth * _distanceInfluenceForSnapDuration(distanceRatio);
int duration;
if (velocity.abs() > 0) {
duration = 4 * (1000 * (distance / velocity).abs()).round();
} else {
final double pageDelta = _getPageDelta(position, tolerance, velocity).abs();
// A slightly different algorithm than on Android, because
// Flutter has different velocity estimate, which is more likely to
// return zero pointer velocity when user holds his finger after a fling,
// compared to Android's VelocityTracker.
//
// It was not clear why exactly this happens, since the estimate logic is
// the same as on Android, so it was decided to adjust this formula
// to produce similar results.
//
// On Android it looks like this:
// duration = ((pageDelta + 1) * 100).toInt();
duration = ((pageDelta * 100 + 1) * 100).toInt();
}
duration = math.min(duration, _kMaxSettleDuration);
return PageScrollSimulation(
position: position.pixels,
target: target,
duration: duration / 1000,
);
}
return null;
}

// See Android ViewPager distanceInfluenceForSnapDuration.
//
// We want the duration of the page snap animation to be influenced by the distance that
// the screen has to travel, however, we don't want this duration to be effected in a
// purely linear fashion. Instead, we use this method to moderate the effect that the distance
// of travel has on the overall snap duration.
double _distanceInfluenceForSnapDuration(double value) {
value -= 0.5; // center the values about 0.
value *= 0.3 * math.pi / 2.0;
return math.sin(value);
}

@override
bool get allowImplicitScrolling => false;
}
Expand Down
4 changes: 3 additions & 1 deletion packages/flutter/lib/src/widgets/scroll_physics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,9 @@ class ScrollPhysics {
ratio: 1.1,
);

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

/// The default accuracy to which scrolling is computed.
Expand Down
69 changes: 69 additions & 0 deletions packages/flutter/lib/src/widgets/scroll_simulation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:flutter/physics.dart';
/// See also:
///
/// * [ClampingScrollSimulation], which implements Android scroll physics.
/// * [PageScrollSimulation], which implements Android page view scroll physics.
class BouncingScrollSimulation extends Simulation {
/// Creates a simulation group for scrolling on iOS, with the given
/// parameters.
Expand Down Expand Up @@ -134,6 +135,7 @@ class BouncingScrollSimulation extends Simulation {
/// See also:
///
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
/// * [PageScrollSimulation], which implements Android page view scroll physics.
//
// This class is based on Scroller.java from Android:
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget
Expand Down Expand Up @@ -229,3 +231,70 @@ class ClampingScrollSimulation extends Simulation {
return time >= _duration;
}
}

/// An implementation of scroll physics that matches Android page view.
///
/// See also:
///
/// * [BouncingScrollSimulation], which implements iOS scroll physics.
/// * [ClampingScrollSimulation], which implements Android scroll physics.
//
// This class ports Android logic from Scroller.java `startScroll`, applying
// the interpolator set in ViewPager.java.
//
// See Android Scroller.java `startScroll` source code
// https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Scroller.java;l=393;drc=ae5bcf23b5f0875e455790d6af387184dbd009c1
class PageScrollSimulation extends Simulation {
/// Creates a scroll physics simulation that matches Android page view.
PageScrollSimulation({
required this.position,
required this.target,
required this.duration,
}) : assert(position != target),
_delta = target - position,
_durationReciprocal = 1.0 / duration;

/// The position at the beginning of the simulation.
final double position;

/// The target, which will be reached at the end of the simulation.
final double target;

/// The duration of the simulation in seconds.
final double duration;

final double _delta;
final double _durationReciprocal;

/// See Android ViewPager interpolator
/// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:viewpager/viewpager/src/main/java/androidx/viewpager/widget/ViewPager.java;l=152;drc=1dcb8847e7aca80ee78c5d9864329b93dd276379
double _interpolate(double t) {
// y = (t - 1)^5 + 1.0
t -= 1.0;
return t * t * t * t * t + 1.0;
}

/// A derivative from [_interpolate] function.
double _interpolateDx(double t) {
// y = 5 * (t - 1)^4
t -= 1.0;
return 5.0 * t * t * t * t;
}

@override
double x(double time) {
time = time.clamp(0.0, duration);
return position + _delta * _interpolate(time * _durationReciprocal);
}

@override
double dx(double time) {
time = time.clamp(0.0, duration);
return _delta * _interpolateDx(time * _durationReciprocal);
}

@override
bool isDone(double time) {
return time >= duration;
}
}
Loading