2424import androidx .core .view .ViewCompat ;
2525import com .facebook .common .logging .FLog ;
2626import com .facebook .infer .annotation .Assertions ;
27+ import com .facebook .react .bridge .WritableMap ;
28+ import com .facebook .react .bridge .WritableNativeMap ;
2729import com .facebook .react .common .ReactConstants ;
2830import com .facebook .react .config .ReactFeatureFlags ;
2931import com .facebook .react .uimanager .MeasureSpecAssertions ;
32+ import com .facebook .react .uimanager .PixelUtil ;
3033import com .facebook .react .uimanager .ReactClippingViewGroup ;
3134import com .facebook .react .uimanager .ReactClippingViewGroupHelper ;
35+ import com .facebook .react .uimanager .StateWrapper ;
3236import com .facebook .react .uimanager .ViewProps ;
3337import com .facebook .react .uimanager .events .NativeGestureUtil ;
3438import com .facebook .react .views .view .ReactViewBackgroundManager ;
@@ -43,6 +47,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
4347
4448 private static @ Nullable Field sScrollerField ;
4549 private static boolean sTriedToGetScrollerField = false ;
50+ private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft" ;
51+ private static final String CONTENT_OFFSET_TOP = "contentOffsetTop" ;
4652
4753 private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper ();
4854 private final @ Nullable OverScroller mScroller ;
@@ -70,6 +76,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
7076 private boolean mSnapToEnd = true ;
7177 private ReactViewBackgroundManager mReactBackgroundManager ;
7278 private boolean mPagedArrowScrolling = false ;
79+ private @ Nullable StateWrapper mStateWrapper ;
7380
7481 private final Rect mTempRect = new Rect ();
7582
@@ -217,7 +224,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
217224 @ Override
218225 protected void onLayout (boolean changed , int l , int t , int r , int b ) {
219226 // Call with the present values in order to re-layout if necessary
220- scrollTo (getScrollX (), getScrollY ());
227+ reactScrollTo (getScrollX (), getScrollY ());
221228 }
222229
223230 /**
@@ -383,6 +390,8 @@ public boolean onTouchEvent(MotionEvent ev) {
383390 mVelocityHelper .calculateVelocity (ev );
384391 int action = ev .getAction () & MotionEvent .ACTION_MASK ;
385392 if (action == MotionEvent .ACTION_UP && mDragging ) {
393+ updateStateOnScroll (getScrollX (), getScrollY ());
394+
386395 float velocityX = mVelocityHelper .getXVelocity ();
387396 float velocityY = mVelocityHelper .getYVelocity ();
388397 ReactScrollViewHelper .emitScrollEndDragEvent (this , velocityX , velocityY );
@@ -605,6 +614,8 @@ public void run() {
605614 ViewCompat .postOnAnimationDelayed (
606615 ReactHorizontalScrollView .this , this , ReactScrollViewHelper .MOMENTUM_DELAY );
607616 } else {
617+ updateStateOnScroll (getScrollX (), getScrollY ());
618+
608619 if (mPagingEnabled && !mSnappingToPage ) {
609620 // Only if we have pagingEnabled and we have not snapped to the page do we
610621 // need to continue checking for the scroll. And we cause that scroll by asking for
@@ -698,7 +709,7 @@ private void smoothScrollAndSnap(int velocity) {
698709 targetOffset = currentPage * interval ;
699710 if (targetOffset != currentOffset ) {
700711 mActivelyScrolling = true ;
701- smoothScrollTo ((int ) targetOffset , getScrollY ());
712+ reactSmoothScrollTo ((int ) targetOffset , getScrollY ());
702713 }
703714 }
704715
@@ -834,7 +845,7 @@ private void flingAndSnap(int velocityX) {
834845
835846 postInvalidateOnAnimation ();
836847 } else {
837- smoothScrollTo (targetOffset , getScrollY ());
848+ reactSmoothScrollTo (targetOffset , getScrollY ());
838849 }
839850 }
840851
@@ -857,7 +868,7 @@ private void smoothScrollToNextPage(int direction) {
857868 page = 0 ;
858869 }
859870
860- smoothScrollTo (page * width , getScrollY ());
871+ reactSmoothScrollTo (page * width , getScrollY ());
861872 handlePostTouchScrolling (0 , 0 );
862873 }
863874
@@ -885,4 +896,45 @@ public void setBorderRadius(float borderRadius, int position) {
885896 public void setBorderStyle (@ Nullable String style ) {
886897 mReactBackgroundManager .setBorderStyle (style );
887898 }
899+
900+ /**
901+ * Calls `smoothScrollTo` and updates state.
902+ *
903+ * <p>`smoothScrollTo` changes `contentOffset` and we need to keep `contentOffset` in sync between
904+ * scroll view and state. Calling raw `smoothScrollTo` doesn't update state.
905+ */
906+ public void reactSmoothScrollTo (int x , int y ) {
907+ smoothScrollTo (x , y );
908+ updateStateOnScroll (x , y );
909+ }
910+
911+ /**
912+ * Calls `reactScrollTo` and updates state.
913+ *
914+ * <p>`reactScrollTo` changes `contentOffset` and we need to keep `contentOffset` in sync between
915+ * scroll view and state. Calling raw `reactScrollTo` doesn't update state.
916+ */
917+ public void reactScrollTo (int x , int y ) {
918+ scrollTo (x , y );
919+ updateStateOnScroll (x , y );
920+ }
921+
922+ public void updateState (@ Nullable StateWrapper stateWrapper ) {
923+ mStateWrapper = stateWrapper ;
924+ }
925+
926+ /**
927+ * Called on any stabilized onScroll change to propagate content offset value to a Shadow Node.
928+ */
929+ private void updateStateOnScroll (int scrollX , int scrollY ) {
930+ if (mStateWrapper == null ) {
931+ return ;
932+ }
933+
934+ WritableMap map = new WritableNativeMap ();
935+ map .putDouble (CONTENT_OFFSET_LEFT , PixelUtil .toDIPFromPixel (scrollX ));
936+ map .putDouble (CONTENT_OFFSET_TOP , PixelUtil .toDIPFromPixel (scrollY ));
937+
938+ mStateWrapper .updateState (map );
939+ }
888940}
0 commit comments