diff --git a/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js b/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js index 22da77e56f1478..2fed508a35adf6 100644 --- a/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +++ b/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js @@ -37,6 +37,7 @@ const AndroidHorizontalScrollViewViewConfig = { snapToInterval: true, snapToStart: true, snapToOffsets: true, + contentOffset: true, }, }; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java index 0dee6f7c8e9d03..c905c5dc9a4f37 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java @@ -50,6 +50,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft"; private static final String CONTENT_OFFSET_TOP = "contentOffsetTop"; + private static final int UNSET_CONTENT_OFFSET = -1; + private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); private final @Nullable OverScroller mScroller; private final VelocityHelper mVelocityHelper = new VelocityHelper(); @@ -76,6 +78,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView private boolean mSnapToEnd = true; private ReactViewBackgroundManager mReactBackgroundManager; private boolean mPagedArrowScrolling = false; + private int pendingContentOffsetX = UNSET_CONTENT_OFFSET; + private int pendingContentOffsetY = UNSET_CONTENT_OFFSET; private @Nullable StateWrapper mStateWrapper; private final Rect mTempRect = new Rect(); @@ -224,7 +228,13 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // Call with the present values in order to re-layout if necessary - reactScrollTo(getScrollX(), getScrollY()); + // If a "pending" value has been set, we restore that value. + // That value gets cleared by reactScrollTo. + int scrollToX = + pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX(); + int scrollToY = + pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY(); + reactScrollTo(scrollToX, scrollToY); } /** @@ -906,6 +916,7 @@ public void setBorderStyle(@Nullable String style) { public void reactSmoothScrollTo(int x, int y) { smoothScrollTo(x, y); updateStateOnScroll(x, y); + setPendingContentOffsets(x, y); } /** @@ -917,6 +928,25 @@ public void reactSmoothScrollTo(int x, int y) { public void reactScrollTo(int x, int y) { scrollTo(x, y); updateStateOnScroll(x, y); + setPendingContentOffsets(x, y); + } + + /** + * If contentOffset is set before the View has been laid out, store the values and set them when + * `onLayout` is called. + * + * @param x + * @param y + */ + private void setPendingContentOffsets(int x, int y) { + View child = getChildAt(0); + if (child != null && child.getWidth() != 0 && child.getHeight() != 0) { + pendingContentOffsetX = UNSET_CONTENT_OFFSET; + pendingContentOffsetY = UNSET_CONTENT_OFFSET; + } else { + pendingContentOffsetX = x; + pendingContentOffsetY = y; + } } public void updateState(@Nullable StateWrapper stateWrapper) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java index b7712b3d65387a..8c2b91637ae1fa 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.java @@ -12,6 +12,7 @@ import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.uimanager.DisplayMetricsHolder; import com.facebook.react.uimanager.PixelUtil; @@ -299,4 +300,11 @@ public void setFadingEdgeLength(ReactHorizontalScrollView view, int value) { view.setFadingEdgeLength(0); } } + + @ReactProp(name = "contentOffset") + public void setContentOffset(ReactHorizontalScrollView view, ReadableMap value) { + double x = value.getDouble("x"); + double y = value.getDouble("y"); + view.reactScrollTo((int) PixelUtil.toPixelFromDIP(x), (int) PixelUtil.toPixelFromDIP(y)); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java index 6bd0be836203e1..f0d4e99658553a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java @@ -55,6 +55,8 @@ public class ReactScrollView extends ScrollView private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft"; private static final String CONTENT_OFFSET_TOP = "contentOffsetTop"; + private static final int UNSET_CONTENT_OFFSET = -1; + private final OnScrollDispatchHelper mOnScrollDispatchHelper = new OnScrollDispatchHelper(); private final @Nullable OverScroller mScroller; private final VelocityHelper mVelocityHelper = new VelocityHelper(); @@ -81,6 +83,8 @@ public class ReactScrollView extends ScrollView private boolean mSnapToEnd = true; private View mContentView; private ReactViewBackgroundManager mReactBackgroundManager; + private int pendingContentOffsetX = UNSET_CONTENT_OFFSET; + private int pendingContentOffsetY = UNSET_CONTENT_OFFSET; private @Nullable StateWrapper mStateWrapper; public ReactScrollView(ReactContext context) { @@ -200,7 +204,13 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // Call with the present values in order to re-layout if necessary - reactScrollTo(getScrollX(), getScrollY()); + // If a "pending" value has been set, we restore that value. + // That value gets cleared by reactScrollTo. + int scrollToX = + pendingContentOffsetX != UNSET_CONTENT_OFFSET ? pendingContentOffsetX : getScrollX(); + int scrollToY = + pendingContentOffsetY != UNSET_CONTENT_OFFSET ? pendingContentOffsetY : getScrollY(); + reactScrollTo(scrollToX, scrollToY); } @Override @@ -777,6 +787,7 @@ public void onChildViewRemoved(View parent, View child) { public void reactSmoothScrollTo(int x, int y) { smoothScrollTo(x, y); updateStateOnScroll(x, y); + setPendingContentOffsets(x, y); } /** @@ -788,6 +799,25 @@ public void reactSmoothScrollTo(int x, int y) { public void reactScrollTo(int x, int y) { scrollTo(x, y); updateStateOnScroll(x, y); + setPendingContentOffsets(x, y); + } + + /** + * If contentOffset is set before the View has been laid out, store the values and set them when + * `onLayout` is called. + * + * @param x + * @param y + */ + private void setPendingContentOffsets(int x, int y) { + View child = getChildAt(0); + if (child != null && child.getWidth() != 0 && child.getHeight() != 0) { + pendingContentOffsetX = UNSET_CONTENT_OFFSET; + pendingContentOffsetY = UNSET_CONTENT_OFFSET; + } else { + pendingContentOffsetX = x; + pendingContentOffsetY = y; + } } /**