Skip to content

Commit

Permalink
Fix StretchingOverscrollIndicator not handling directional changes …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
jankuss authored Feb 15, 2023
1 parent 1f85497 commit 4ad47fb
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 21 deletions.
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,5 @@ Junhua Lin <1075209054@qq.com>
Tomasz Gucio <tgucio@gmail.com>
Jason C.H <ctrysbita@outlook.com>
Hubert Jóźwiak <hjozwiakdx@gmail.com>
Eli Albert <crasowas@gmail.com>
Eli Albert <crasowas@gmail.com>
Jan Kuß <jan@kuss.dev>
55 changes: 43 additions & 12 deletions packages/flutter/lib/src/widgets/overscroll_indicator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -706,48 +718,52 @@ 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) {
// We clamp the overscroll amount relative to the length of the viewport,
// 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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -847,23 +866,35 @@ 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;
_stretchSizeTween.end = math.min(_stretchIntensity + (_flingFriction / velocity), 1.0);
_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.
///
/// 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;
Expand Down
Loading

0 comments on commit 4ad47fb

Please sign in to comment.