Skip to content

Commit

Permalink
Support contentOffset property in Android's ScrollView and Horizont…
Browse files Browse the repository at this point in the history
…alScrollView

Summary:
For a very long time, iOS has supported the `contentOffset` property but Android has not:

#6849

This property can be used, primarily, to autoscroll the ScrollView to a starting position when it is first rendered, to avoid "jumps" that occur by asynchronously scrolling to a start position.

Changelog: [Android][Changed] ScrollView now supports `contentOffset`

Reviewed By: mdvacca

Differential Revision: D21198236

fbshipit-source-id: 2b0773569ba42120cb1fcf0f3847ca98af2285e7
  • Loading branch information
JoshuaGross authored and facebook-github-bot committed Apr 23, 2020
1 parent bda8aae commit ed29ba1
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const AndroidHorizontalScrollViewViewConfig = {
snapToInterval: true,
snapToStart: true,
snapToOffsets: true,
contentOffset: true,
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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;
}
}

/**
Expand Down

0 comments on commit ed29ba1

Please sign in to comment.