7
7
8
8
package com .facebook .react .views .scroll ;
9
9
10
+ import android .animation .Animator ;
11
+ import android .animation .ObjectAnimator ;
12
+ import android .animation .PropertyValuesHolder ;
13
+ import android .animation .ValueAnimator ;
10
14
import android .content .Context ;
11
15
import android .graphics .Canvas ;
12
16
import android .graphics .Color ;
13
17
import android .graphics .Rect ;
14
18
import android .graphics .drawable .ColorDrawable ;
15
19
import android .graphics .drawable .Drawable ;
20
+ import android .os .Build ;
16
21
import android .view .FocusFinder ;
17
22
import android .view .KeyEvent ;
18
23
import android .view .MotionEvent ;
@@ -82,6 +87,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
82
87
private int pendingContentOffsetY = UNSET_CONTENT_OFFSET ;
83
88
private @ Nullable StateWrapper mStateWrapper ;
84
89
90
+ private @ Nullable ValueAnimator mScrollAnimator ;
91
+ private int mFinalAnimatedPositionScrollX = 0 ;
92
+ private int mFinalAnimatedPositionScrollY = 0 ;
93
+
85
94
private final Rect mTempRect = new Rect ();
86
95
87
96
public ReactHorizontalScrollView (Context context ) {
@@ -648,6 +657,20 @@ public void run() {
648
657
ReactHorizontalScrollView .this , mPostTouchRunnable , ReactScrollViewHelper .MOMENTUM_DELAY );
649
658
}
650
659
660
+ /** Get current X position or position after current animation finishes, if any. */
661
+ private int getPostAnimationScrollX () {
662
+ return mScrollAnimator != null && mScrollAnimator .isRunning ()
663
+ ? mFinalAnimatedPositionScrollX
664
+ : getScrollX ();
665
+ }
666
+
667
+ /** Get current X position or position after current animation finishes, if any. */
668
+ private int getPostAnimationScrollY () {
669
+ return mScrollAnimator != null && mScrollAnimator .isRunning ()
670
+ ? mFinalAnimatedPositionScrollY
671
+ : getScrollY ();
672
+ }
673
+
651
674
private int predictFinalScrollPosition (int velocityX ) {
652
675
// ScrollView can *only* scroll for 250ms when using smoothScrollTo and there's
653
676
// no way to customize the scroll duration. So, we create a temporary OverScroller
@@ -659,8 +682,8 @@ private int predictFinalScrollPosition(int velocityX) {
659
682
int maximumOffset = Math .max (0 , computeHorizontalScrollRange () - getWidth ());
660
683
int width = getWidth () - ViewCompat .getPaddingStart (this ) - ViewCompat .getPaddingEnd (this );
661
684
scroller .fling (
662
- getScrollX (), // startX
663
- getScrollY (), // startY
685
+ getPostAnimationScrollX (), // startX
686
+ getPostAnimationScrollY (), // startY
664
687
velocityX , // velocityX
665
688
0 , // velocityY
666
689
0 , // minX
@@ -674,13 +697,13 @@ private int predictFinalScrollPosition(int velocityX) {
674
697
}
675
698
676
699
/**
677
- * This will smooth scroll us to the nearest snap offset point It currently just looks at where
700
+ * This will smooth scroll us to the nearest snap offset point. It currently just looks at where
678
701
* the content is and slides to the nearest point. It is intended to be run after we are done
679
702
* scrolling, and handling any momentum scrolling.
680
703
*/
681
704
private void smoothScrollAndSnap (int velocity ) {
682
705
double interval = (double ) getSnapInterval ();
683
- double currentOffset = (double ) getScrollX ( );
706
+ double currentOffset = (double ) ( getPostAnimationScrollX () );
684
707
double targetOffset = (double ) predictFinalScrollPosition (velocity );
685
708
686
709
int previousPage = (int ) Math .floor (currentOffset / interval );
@@ -914,7 +937,54 @@ public void setBorderStyle(@Nullable String style) {
914
937
* scroll view and state. Calling raw `smoothScrollTo` doesn't update state.
915
938
*/
916
939
public void reactSmoothScrollTo (int x , int y ) {
917
- smoothScrollTo (x , y );
940
+ // `smoothScrollTo` contains some logic that, if called multiple times in a short amount of
941
+ // time, will treat all calls as part of the same animation and will not lengthen the duration
942
+ // of the animation. This means that, for example, if the user is scrolling rapidly, multiple
943
+ // pages could be considered part of one animation, causing some page animations to be animated
944
+ // very rapidly - looking like they're not animated at all.
945
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .HONEYCOMB ) {
946
+ if (mScrollAnimator != null ) {
947
+ mScrollAnimator .cancel ();
948
+ }
949
+
950
+ mFinalAnimatedPositionScrollX = x ;
951
+ mFinalAnimatedPositionScrollY = y ;
952
+ PropertyValuesHolder scrollX = PropertyValuesHolder .ofInt ("scrollX" , getScrollX (), x );
953
+ PropertyValuesHolder scrollY = PropertyValuesHolder .ofInt ("scrollY" , getScrollY (), y );
954
+ mScrollAnimator = ObjectAnimator .ofPropertyValuesHolder (scrollX , scrollY );
955
+ mScrollAnimator .setDuration (
956
+ ReactScrollViewHelper .getDefaultScrollAnimationDuration (getContext ()));
957
+ mScrollAnimator .addUpdateListener (
958
+ new ValueAnimator .AnimatorUpdateListener () {
959
+ @ Override
960
+ public void onAnimationUpdate (ValueAnimator valueAnimator ) {
961
+ int scrollValueX = (Integer ) valueAnimator .getAnimatedValue ("scrollX" );
962
+ int scrollValueY = (Integer ) valueAnimator .getAnimatedValue ("scrollY" );
963
+ ReactHorizontalScrollView .this .scrollTo (scrollValueX , scrollValueY );
964
+ }
965
+ });
966
+ mScrollAnimator .addListener (
967
+ new Animator .AnimatorListener () {
968
+ @ Override
969
+ public void onAnimationStart (Animator animator ) {}
970
+
971
+ @ Override
972
+ public void onAnimationEnd (Animator animator ) {
973
+ mFinalAnimatedPositionScrollX = -1 ;
974
+ mFinalAnimatedPositionScrollY = -1 ;
975
+ mScrollAnimator = null ;
976
+ }
977
+
978
+ @ Override
979
+ public void onAnimationCancel (Animator animator ) {}
980
+
981
+ @ Override
982
+ public void onAnimationRepeat (Animator animator ) {}
983
+ });
984
+ mScrollAnimator .start ();
985
+ } else {
986
+ smoothScrollTo (x , y );
987
+ }
918
988
updateStateOnScroll (x , y );
919
989
setPendingContentOffsets (x , y );
920
990
}
0 commit comments