diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroupHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroupHelper.java index 393b7cc602b..8c9399073af 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroupHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroupHelper.java @@ -43,12 +43,16 @@ public class ReactClippingViewGroupHelper { // Intersect the view with the parent's rectangle // This will result in the overlap with coordinates in the parent space if (!sHelperRect.intersect( - view.getLeft(), view.getTop(), view.getRight(), view.getBottom())) { + view.getLeft(), + view.getTop() + (int) view.getTranslationY(), + view.getRight(), + view.getBottom() + (int) view.getTranslationY())) { outputRect.setEmpty(); return; } // Now we move the coordinates to the View's coordinate space sHelperRect.offset(-view.getLeft(), -view.getTop()); + sHelperRect.offset(-(int) view.getTranslationX(), -(int) view.getTranslationY()); sHelperRect.offset(view.getScrollX(), view.getScrollY()); outputRect.set(sHelperRect); return; 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 ac383b96966..0304f0e0678 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 @@ -60,6 +60,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView private static boolean sTriedToGetScrollerField = false; private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft"; private static final String CONTENT_OFFSET_TOP = "contentOffsetTop"; + private static final String SCROLL_AWAY_PADDING_TOP = "scrollAwayPaddingTop"; private int mLayoutDirection; private int mScrollXAfterMeasure = NO_SCROLL_POSITION; @@ -1214,6 +1215,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView WritableMap map = new WritableNativeMap(); map.putDouble(CONTENT_OFFSET_LEFT, PixelUtil.toDIPFromPixel(fabricScrollX)); map.putDouble(CONTENT_OFFSET_TOP, PixelUtil.toDIPFromPixel(scrollY)); + map.putDouble(SCROLL_AWAY_PADDING_TOP, 0); return map; } }); 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 90c1ef8283d..21a99dfd81b 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 @@ -58,6 +58,7 @@ public class ReactScrollView extends ScrollView private static boolean sTriedToGetScrollerField = false; private static final String CONTENT_OFFSET_LEFT = "contentOffsetLeft"; private static final String CONTENT_OFFSET_TOP = "contentOffsetTop"; + private static final String SCROLL_AWAY_PADDING_TOP = "scrollAwayPaddingTop"; private static final int UNSET_CONTENT_OFFSET = -1; @@ -95,6 +96,8 @@ public class ReactScrollView extends ScrollView private int mFinalAnimatedPositionScrollX; private int mFinalAnimatedPositionScrollY; + private int mScrollAwayPaddingTop = 0; + private int mLastStateUpdateScrollX = -1; private int mLastStateUpdateScrollY = -1; @@ -959,6 +962,41 @@ public class ReactScrollView extends ScrollView mReactBackgroundManager.setBorderStyle(style); } + /** + * ScrollAway: This enables a natively-controlled navbar that optionally obscures the top content + * of the ScrollView. Whether or not the navbar is obscuring the React Native surface is + * determined outside of React Native. + * + *
Note: all ScrollViews and HorizontalScrollViews in React have exactly one child: the + * "content" View (see ScrollView.js). That View is non-collapsable so it will never be + * View-flattened away. However, it is possible to pass custom styles into that View. + * + *
If you are using this feature it is assumed that you have full control over this ScrollView + * and that you are **not** overriding the ScrollView content view to pass in a `translateY` + * style. `translateY` must never be set from ReactJS while using this feature! + */ + public void setScrollAwayTopPaddingEnabledUnstable(int topPadding) { + int count = getChildCount(); + + Assertions.assertCondition( + count == 1, "React Native ScrollView always has exactly 1 child; a content View"); + + if (count > 0) { + for (int i = 0; i < count; i++) { + View childView = getChildAt(i); + childView.setTranslationY(topPadding); + } + + // Add the topPadding value as the bottom padding for the ScrollView. + // Otherwise, we'll push down the contents of the scroll view down too + // far off screen. + setPadding(0, 0, 0, topPadding); + } + + updateScrollAwayState(topPadding); + setRemoveClippedSubviews(mRemoveClippedSubviews); + } + /** * Called on any stabilized onScroll change to propagate content offset value to a Shadow Node. */ @@ -971,23 +1009,41 @@ public class ReactScrollView extends ScrollView mLastStateUpdateScrollX = scrollX; mLastStateUpdateScrollY = scrollY; - mFabricViewStateManager.setState( - new FabricViewStateManager.StateUpdateCallback() { - @Override - public WritableMap getStateUpdate() { - - WritableMap map = new WritableNativeMap(); - map.putDouble(CONTENT_OFFSET_LEFT, PixelUtil.toDIPFromPixel(scrollX)); - map.putDouble(CONTENT_OFFSET_TOP, PixelUtil.toDIPFromPixel(scrollY)); - return map; - } - }); + forceUpdateState(); } private void updateStateOnScroll() { updateStateOnScroll(getScrollX(), getScrollY()); } + private void updateScrollAwayState(int scrollAwayPaddingTop) { + if (mScrollAwayPaddingTop == scrollAwayPaddingTop) { + return; + } + + mScrollAwayPaddingTop = scrollAwayPaddingTop; + + forceUpdateState(); + } + + private void forceUpdateState() { + final int scrollX = mLastStateUpdateScrollX; + final int scrollY = mLastStateUpdateScrollY; + final int scrollAwayPaddingTop = mScrollAwayPaddingTop; + + mFabricViewStateManager.setState( + new FabricViewStateManager.StateUpdateCallback() { + @Override + public WritableMap getStateUpdate() { + WritableMap map = new WritableNativeMap(); + map.putDouble(CONTENT_OFFSET_LEFT, PixelUtil.toDIPFromPixel(scrollX)); + map.putDouble(CONTENT_OFFSET_TOP, PixelUtil.toDIPFromPixel(scrollY)); + map.putDouble(SCROLL_AWAY_PADDING_TOP, PixelUtil.toDIPFromPixel(scrollAwayPaddingTop)); + return map; + } + }); + } + @Override public FabricViewStateManager getFabricViewStateManager() { return mFabricViewStateManager; diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp b/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp index e5faef33faf..09c860740f3 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp @@ -34,7 +34,7 @@ void ScrollViewShadowNode::updateStateIfNeeded() { void ScrollViewShadowNode::updateScrollContentOffsetIfNeeded() { #ifndef ANDROID if (getLayoutMetrics().layoutDirection == LayoutDirection::RightToLeft) { - // Yoga place `contentView` on the right side of `scrollView` when RTL + // Yoga places `contentView` on the right side of `scrollView` when RTL // layout is enforced. To correct for this, in RTL setting, correct the // frame's origin. React Native Classic does this as well in // `RCTScrollContentShadowView.m`. @@ -58,8 +58,9 @@ void ScrollViewShadowNode::layout(LayoutContext layoutContext) { } Point ScrollViewShadowNode::getContentOriginOffset() const { - auto contentOffset = getStateData().contentOffset; - return {-contentOffset.x, -contentOffset.y}; + auto stateData = getStateData(); + auto contentOffset = stateData.contentOffset; + return {-contentOffset.x, -contentOffset.y + stateData.scrollAwayPaddingTop}; } } // namespace react diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h b/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h index 3f979ab3f59..6d3dd7e6325 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h @@ -25,6 +25,7 @@ class ScrollViewState final { public: Point contentOffset; Rect contentBoundingRect; + int scrollAwayPaddingTop; /* * Returns size of scrollable area. @@ -37,11 +38,13 @@ class ScrollViewState final { : contentOffset( {(Float)data["contentOffsetLeft"].getDouble(), (Float)data["contentOffsetTop"].getDouble()}), - contentBoundingRect({}){}; + contentBoundingRect({}), + scrollAwayPaddingTop((Float)data["scrollAwayPaddingTop"].getDouble()){}; folly::dynamic getDynamic() const { return folly::dynamic::object("contentOffsetLeft", contentOffset.x)( - "contentOffsetTop", contentOffset.y); + "contentOffsetTop", contentOffset.y)( + "scrollAwayPaddingTop", scrollAwayPaddingTop); }; MapBuffer getMapBuffer() const { return MapBufferBuilder::EMPTY(); diff --git a/ReactCommon/react/renderer/core/tests/ConcreteShadowNodeTest.cpp b/ReactCommon/react/renderer/core/tests/ConcreteShadowNodeTest.cpp index de48a4b6e7b..0b2d025d08a 100644 --- a/ReactCommon/react/renderer/core/tests/ConcreteShadowNodeTest.cpp +++ b/ReactCommon/react/renderer/core/tests/ConcreteShadowNodeTest.cpp @@ -26,7 +26,7 @@ TEST(ConcreteShadowNodeTest, testSetStateData) { auto shadowNode = builder.build(element); - shadowNode->setStateData({{10, 11}, {{21, 22}, {301, 302}}}); + shadowNode->setStateData({{10, 11}, {{21, 22}, {301, 302}}, 0}); EXPECT_NE( shadowNode->getState(), shadowNode->getFamily().getMostRecentState());