Skip to content

Commit

Permalink
Support ScrollAway in ReactScrollView
Browse files Browse the repository at this point in the history
Summary:
Support ScrollAway in ReactScrollView for Fabric/non-Fabric.

Changelog: [Android][Added] Support for ScrollAway native nav bars added to ReactScrollView

Reviewed By: mdvacca

Differential Revision: D28308855

fbshipit-source-id: 9a922159ef50fb7c8e9c484a4b97ca57ab248496
  • Loading branch information
JoshuaGross authored and facebook-github-bot committed May 10, 2021
1 parent d87542e commit 0ef5bee
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,16 @@ public static void calculateClippingRect(View view, Rect outputRect) {
// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1214,6 +1215,7 @@ public WritableMap getStateUpdate() {
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;
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -959,6 +962,41 @@ public void setBorderStyle(@Nullable String style) {
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.
*
* <p>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.
*
* <p>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.
*/
Expand All @@ -971,23 +1009,41 @@ private void updateStateOnScroll(final int scrollX, final int scrollY) {
mLastStateUpdateScrollX = scrollX;
mLastStateUpdateScrollY = scrollY;

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;
}
});
}

private void updateStateOnScroll() {
updateStateOnScroll(getScrollX(), getScrollY());
}

@Override
public FabricViewStateManager getFabricViewStateManager() {
return mFabricViewStateManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ScrollViewState final {
public:
Point contentOffset;
Rect contentBoundingRect;
int scrollAwayPaddingTop;

/*
* Returns size of scrollable area.
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down

0 comments on commit 0ef5bee

Please sign in to comment.