Unverified Commit 4ad47fb4 authored by Jan Kuß's avatar Jan Kuß Committed by GitHub

Fix `StretchingOverscrollIndicator` not handling directional changes correctly (#116548)

* Fix overscroll behaviour of `StretchingOverscrollIndicator` on directional change while scrolling

* Remove trailing spaces

* Change naming of `_StretchDirection` values to `trailing` and `leading`

* Remove trailing space

* Apply suggestions from code review

* Fix stretching overscroll indicator direction on fling

* Fix issue when changing direction while recede animation is playing

* Remove trailing space

* Add test for changing direction during recede animation
parent 1f85497e
......@@ -102,3 +102,4 @@ Tomasz Gucio <tgucio@gmail.com>
Jason C.H <ctrysbita@outlook.com>
Hubert Jóźwiak <hjozwiakdx@gmail.com>
Eli Albert <crasowas@gmail.com>
Jan Kuß <jan@kuss.dev>
......@@ -601,6 +601,15 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter {
}
}
enum _StretchDirection {
/// The [trailing] direction indicates that the content will be stretched toward
/// the trailing edge.
trailing,
/// The [leading] direction indicates that the content will be stretched toward
/// the leading edge.
leading,
}
/// A Material Design visual indication that a scroll view has overscrolled.
///
/// A [StretchingOverscrollIndicator] listens for [ScrollNotification]s in order
......@@ -689,6 +698,9 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
late final _StretchController _stretchController = _StretchController(vsync: this);
ScrollNotification? _lastNotification;
OverscrollNotification? _lastOverscrollNotification;
double _totalOverscroll = 0.0;
bool _accepted = true;
bool _handleScrollNotification(ScrollNotification notification) {
......@@ -706,9 +718,11 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
assert(notification.metrics.axis == widget.axis);
if (_accepted) {
_totalOverscroll += notification.overscroll;
if (notification.velocity != 0.0) {
assert(notification.dragDetails == null);
_stretchController.absorbImpact(notification.velocity.abs());
_stretchController.absorbImpact(notification.velocity.abs(), _totalOverscroll);
} else {
assert(notification.overscroll != 0.0);
if (notification.dragDetails != null) {
......@@ -716,38 +730,40 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
// which is the furthest distance a single pointer could pull on the
// screen. This is because more than one pointer will multiply the
// amount of overscroll - https://github.com/flutter/flutter/issues/11884
final double viewportDimension = notification.metrics.viewportDimension;
final double distanceForPull =
(notification.overscroll.abs() / viewportDimension) + _stretchController.pullDistance;
final double distanceForPull = _totalOverscroll.abs() / viewportDimension;
final double clampedOverscroll = clampDouble(distanceForPull, 0, 1.0);
_stretchController.pull(clampedOverscroll);
_stretchController.pull(clampedOverscroll, _totalOverscroll);
}
}
}
} else if (notification is ScrollEndNotification || notification is ScrollUpdateNotification) {
// Since the overscrolling ended, we reset the total overscroll amount.
_totalOverscroll = 0;
_stretchController.scrollEnd();
}
_lastNotification = notification;
return false;
}
AlignmentGeometry _getAlignmentForAxisDirection(double overscroll) {
AlignmentGeometry _getAlignmentForAxisDirection(_StretchDirection stretchDirection) {
// Accounts for reversed scrollables by checking the AxisDirection
switch (widget.axisDirection) {
case AxisDirection.up:
return overscroll > 0
return stretchDirection == _StretchDirection.trailing
? AlignmentDirectional.topCenter
: AlignmentDirectional.bottomCenter;
case AxisDirection.right:
return overscroll > 0
return stretchDirection == _StretchDirection.trailing
? Alignment.centerRight
: Alignment.centerLeft;
case AxisDirection.down:
return overscroll > 0
return stretchDirection == _StretchDirection.trailing
? AlignmentDirectional.bottomCenter
: AlignmentDirectional.topCenter;
case AxisDirection.left:
return overscroll > 0
return stretchDirection == _StretchDirection.trailing
? Alignment.centerLeft
: Alignment.centerRight;
}
......@@ -784,7 +800,7 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
}
final AlignmentGeometry alignment = _getAlignmentForAxisDirection(
_lastOverscrollNotification?.overscroll ?? 0.0
_stretchController.stretchDirection,
);
final double viewportDimension = _lastOverscrollNotification?.metrics.viewportDimension ?? mainAxisSize;
......@@ -836,6 +852,9 @@ class _StretchController extends ChangeNotifier {
double get pullDistance => _pullDistance;
double _pullDistance = 0.0;
_StretchDirection get stretchDirection => _stretchDirection;
_StretchDirection _stretchDirection = _StretchDirection.trailing;
// Constants from Android.
static const double _exponentialScalar = math.e / 0.33;
static const double _stretchIntensity = 0.016;
......@@ -847,7 +866,7 @@ class _StretchController extends ChangeNotifier {
/// Handle a fling to the edge of the viewport at a particular velocity.
///
/// The velocity must be positive.
void absorbImpact(double velocity) {
void absorbImpact(double velocity, double totalOverscroll) {
assert(velocity >= 0.0);
velocity = clampDouble(velocity, 1, 10000);
_stretchSizeTween.begin = _stretchSize.value;
......@@ -855,6 +874,7 @@ class _StretchController extends ChangeNotifier {
_stretchController.duration = Duration(milliseconds: (velocity * 0.02).round());
_stretchController.forward(from: 0.0);
_state = _StretchState.absorb;
_stretchDirection = totalOverscroll > 0 ? _StretchDirection.trailing : _StretchDirection.leading;
}
/// Handle a user-driven overscroll.
......@@ -862,8 +882,19 @@ class _StretchController extends ChangeNotifier {
/// The `normalizedOverscroll` argument should be the absolute value of the
/// scroll distance in logical pixels, divided by the extent of the viewport
/// in the main axis.
void pull(double normalizedOverscroll) {
void pull(double normalizedOverscroll, double totalOverscroll) {
assert(normalizedOverscroll >= 0.0);
final _StretchDirection newStretchDirection = totalOverscroll > 0 ? _StretchDirection.trailing : _StretchDirection.leading;
if (_stretchDirection != newStretchDirection && _state == _StretchState.recede) {
// When the stretch direction changes while we are in the recede state, we need to ignore the change.
// If we don't, the stretch will instantly jump to the new direction with the recede animation still playing, which causes
// a unwanted visual abnormality (https://github.com/flutter/flutter/pull/116548#issuecomment-1414872567).
// By ignoring the directional change until the recede state is finished, we can avoid this.
return;
}
_stretchDirection = newStretchDirection;
_pullDistance = normalizedOverscroll;
_stretchSizeTween.begin = _stretchSize.value;
final double linearIntensity =_stretchIntensity * _pullDistance;
......
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