@@ -601,6 +601,15 @@ class _GlowingOverscrollIndicatorPainter extends CustomPainter {
601
601
}
602
602
}
603
603
604
+ enum _StretchDirection {
605
+ /// The [trailing] direction indicates that the content will be stretched toward
606
+ /// the trailing edge.
607
+ trailing,
608
+ /// The [leading] direction indicates that the content will be stretched toward
609
+ /// the leading edge.
610
+ leading,
611
+ }
612
+
604
613
/// A Material Design visual indication that a scroll view has overscrolled.
605
614
///
606
615
/// A [StretchingOverscrollIndicator] listens for [ScrollNotification] s in order
@@ -689,6 +698,9 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
689
698
late final _StretchController _stretchController = _StretchController (vsync: this );
690
699
ScrollNotification ? _lastNotification;
691
700
OverscrollNotification ? _lastOverscrollNotification;
701
+
702
+ double _totalOverscroll = 0.0 ;
703
+
692
704
bool _accepted = true ;
693
705
694
706
bool _handleScrollNotification (ScrollNotification notification) {
@@ -706,48 +718,52 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
706
718
707
719
assert (notification.metrics.axis == widget.axis);
708
720
if (_accepted) {
721
+ _totalOverscroll += notification.overscroll;
722
+
709
723
if (notification.velocity != 0.0 ) {
710
724
assert (notification.dragDetails == null );
711
- _stretchController.absorbImpact (notification.velocity.abs ());
725
+ _stretchController.absorbImpact (notification.velocity.abs (), _totalOverscroll );
712
726
} else {
713
727
assert (notification.overscroll != 0.0 );
714
728
if (notification.dragDetails != null ) {
715
729
// We clamp the overscroll amount relative to the length of the viewport,
716
730
// which is the furthest distance a single pointer could pull on the
717
731
// screen. This is because more than one pointer will multiply the
718
732
// amount of overscroll - https://github.com/flutter/flutter/issues/11884
733
+
719
734
final double viewportDimension = notification.metrics.viewportDimension;
720
- final double distanceForPull =
721
- (notification.overscroll.abs () / viewportDimension) + _stretchController.pullDistance;
735
+ final double distanceForPull = _totalOverscroll.abs () / viewportDimension;
722
736
final double clampedOverscroll = clampDouble (distanceForPull, 0 , 1.0 );
723
- _stretchController.pull (clampedOverscroll);
737
+ _stretchController.pull (clampedOverscroll, _totalOverscroll );
724
738
}
725
739
}
726
740
}
727
741
} else if (notification is ScrollEndNotification || notification is ScrollUpdateNotification ) {
742
+ // Since the overscrolling ended, we reset the total overscroll amount.
743
+ _totalOverscroll = 0 ;
728
744
_stretchController.scrollEnd ();
729
745
}
730
746
_lastNotification = notification;
731
747
return false ;
732
748
}
733
749
734
- AlignmentGeometry _getAlignmentForAxisDirection (double overscroll ) {
750
+ AlignmentGeometry _getAlignmentForAxisDirection (_StretchDirection stretchDirection ) {
735
751
// Accounts for reversed scrollables by checking the AxisDirection
736
752
switch (widget.axisDirection) {
737
753
case AxisDirection .up:
738
- return overscroll > 0
754
+ return stretchDirection == _StretchDirection .trailing
739
755
? AlignmentDirectional .topCenter
740
756
: AlignmentDirectional .bottomCenter;
741
757
case AxisDirection .right:
742
- return overscroll > 0
758
+ return stretchDirection == _StretchDirection .trailing
743
759
? Alignment .centerRight
744
760
: Alignment .centerLeft;
745
761
case AxisDirection .down:
746
- return overscroll > 0
762
+ return stretchDirection == _StretchDirection .trailing
747
763
? AlignmentDirectional .bottomCenter
748
764
: AlignmentDirectional .topCenter;
749
765
case AxisDirection .left:
750
- return overscroll > 0
766
+ return stretchDirection == _StretchDirection .trailing
751
767
? Alignment .centerLeft
752
768
: Alignment .centerRight;
753
769
}
@@ -784,7 +800,7 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
784
800
}
785
801
786
802
final AlignmentGeometry alignment = _getAlignmentForAxisDirection (
787
- _lastOverscrollNotification ? .overscroll ?? 0.0
803
+ _stretchController.stretchDirection,
788
804
);
789
805
790
806
final double viewportDimension = _lastOverscrollNotification? .metrics.viewportDimension ?? mainAxisSize;
@@ -836,6 +852,9 @@ class _StretchController extends ChangeNotifier {
836
852
double get pullDistance => _pullDistance;
837
853
double _pullDistance = 0.0 ;
838
854
855
+ _StretchDirection get stretchDirection => _stretchDirection;
856
+ _StretchDirection _stretchDirection = _StretchDirection .trailing;
857
+
839
858
// Constants from Android.
840
859
static const double _exponentialScalar = math.e / 0.33 ;
841
860
static const double _stretchIntensity = 0.016 ;
@@ -847,23 +866,35 @@ class _StretchController extends ChangeNotifier {
847
866
/// Handle a fling to the edge of the viewport at a particular velocity.
848
867
///
849
868
/// The velocity must be positive.
850
- void absorbImpact (double velocity) {
869
+ void absorbImpact (double velocity, double totalOverscroll ) {
851
870
assert (velocity >= 0.0 );
852
871
velocity = clampDouble (velocity, 1 , 10000 );
853
872
_stretchSizeTween.begin = _stretchSize.value;
854
873
_stretchSizeTween.end = math.min (_stretchIntensity + (_flingFriction / velocity), 1.0 );
855
874
_stretchController.duration = Duration (milliseconds: (velocity * 0.02 ).round ());
856
875
_stretchController.forward (from: 0.0 );
857
876
_state = _StretchState .absorb;
877
+ _stretchDirection = totalOverscroll > 0 ? _StretchDirection .trailing : _StretchDirection .leading;
858
878
}
859
879
860
880
/// Handle a user-driven overscroll.
861
881
///
862
882
/// The `normalizedOverscroll` argument should be the absolute value of the
863
883
/// scroll distance in logical pixels, divided by the extent of the viewport
864
884
/// in the main axis.
865
- void pull (double normalizedOverscroll) {
885
+ void pull (double normalizedOverscroll, double totalOverscroll ) {
866
886
assert (normalizedOverscroll >= 0.0 );
887
+
888
+ final _StretchDirection newStretchDirection = totalOverscroll > 0 ? _StretchDirection .trailing : _StretchDirection .leading;
889
+ if (_stretchDirection != newStretchDirection && _state == _StretchState .recede) {
890
+ // When the stretch direction changes while we are in the recede state, we need to ignore the change.
891
+ // If we don't, the stretch will instantly jump to the new direction with the recede animation still playing, which causes
892
+ // a unwanted visual abnormality (https://github.com/flutter/flutter/pull/116548#issuecomment-1414872567).
893
+ // By ignoring the directional change until the recede state is finished, we can avoid this.
894
+ return ;
895
+ }
896
+
897
+ _stretchDirection = newStretchDirection;
867
898
_pullDistance = normalizedOverscroll;
868
899
_stretchSizeTween.begin = _stretchSize.value;
869
900
final double linearIntensity = _stretchIntensity * _pullDistance;
0 commit comments