-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for ScrollView.maintainVisibleContentPosition
on Android
#29466
Changes from all commits
b1df249
8c29447
a2509b3
8d4fd42
99b69db
63cb074
2f3110b
344b9db
84ecefc
9d5eef8
45c5ecf
3992154
19603e1
174f348
2e37d50
04b8905
7a67669
1bc4b8d
4039ecd
9bed499
421f329
7761851
cc6bdf0
3257a58
fca378d
03f7d9d
0a15381
51beb41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -17,10 +17,12 @@ | |||||||||||||||||||
import android.graphics.Rect; | ||||||||||||||||||||
import android.graphics.drawable.ColorDrawable; | ||||||||||||||||||||
import android.graphics.drawable.Drawable; | ||||||||||||||||||||
import android.os.Handler; | ||||||||||||||||||||
import android.view.FocusFinder; | ||||||||||||||||||||
import android.view.KeyEvent; | ||||||||||||||||||||
import android.view.MotionEvent; | ||||||||||||||||||||
import android.view.View; | ||||||||||||||||||||
import android.view.ViewGroup; | ||||||||||||||||||||
import android.view.accessibility.AccessibilityEvent; | ||||||||||||||||||||
import android.widget.HorizontalScrollView; | ||||||||||||||||||||
import android.widget.OverScroller; | ||||||||||||||||||||
|
@@ -44,13 +46,17 @@ | |||||||||||||||||||
import com.facebook.react.uimanager.ViewProps; | ||||||||||||||||||||
import com.facebook.react.uimanager.events.NativeGestureUtil; | ||||||||||||||||||||
import com.facebook.react.views.view.ReactViewBackgroundManager; | ||||||||||||||||||||
import com.facebook.react.views.view.ReactViewGroup; | ||||||||||||||||||||
import java.lang.ref.WeakReference; | ||||||||||||||||||||
import java.lang.reflect.Field; | ||||||||||||||||||||
import java.util.ArrayList; | ||||||||||||||||||||
import java.util.List; | ||||||||||||||||||||
|
||||||||||||||||||||
/** Similar to {@link ReactScrollView} but only supports horizontal scrolling. */ | ||||||||||||||||||||
public class ReactHorizontalScrollView extends HorizontalScrollView | ||||||||||||||||||||
implements ReactClippingViewGroup, | ||||||||||||||||||||
ViewGroup.OnHierarchyChangeListener, | ||||||||||||||||||||
View.OnLayoutChangeListener, | ||||||||||||||||||||
FabricViewStateManager.HasFabricViewStateManager, | ||||||||||||||||||||
ReactOverflowView { | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -94,11 +100,25 @@ public class ReactHorizontalScrollView extends HorizontalScrollView | |||||||||||||||||||
private @Nullable List<Integer> mSnapOffsets; | ||||||||||||||||||||
private boolean mSnapToStart = true; | ||||||||||||||||||||
private boolean mSnapToEnd = true; | ||||||||||||||||||||
private View mContentView; | ||||||||||||||||||||
private ReactViewBackgroundManager mReactBackgroundManager; | ||||||||||||||||||||
private boolean mPagedArrowScrolling = false; | ||||||||||||||||||||
private int pendingContentOffsetX = UNSET_CONTENT_OFFSET; | ||||||||||||||||||||
private int pendingContentOffsetY = UNSET_CONTENT_OFFSET; | ||||||||||||||||||||
private final FabricViewStateManager mFabricViewStateManager = new FabricViewStateManager(); | ||||||||||||||||||||
private @Nullable ReactScrollViewMaintainVisibleContentPositionData | ||||||||||||||||||||
mMaintainVisibleContentPositionData; | ||||||||||||||||||||
private @Nullable WeakReference<View> firstVisibleViewForMaintainVisibleContentPosition = null; | ||||||||||||||||||||
private @Nullable Rect prevFirstVisibleFrameForMaintainVisibleContentPosition = null; | ||||||||||||||||||||
|
||||||||||||||||||||
private final Handler mHandler = new Handler(); | ||||||||||||||||||||
private final Runnable mComputeFirstVisibleViewRunnable = | ||||||||||||||||||||
new Runnable() { | ||||||||||||||||||||
@Override | ||||||||||||||||||||
public void run() { | ||||||||||||||||||||
computeFirstVisibleItemForMaintainVisibleContentPosition(); | ||||||||||||||||||||
} | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
private @Nullable ValueAnimator mScrollAnimator; | ||||||||||||||||||||
private int mFinalAnimatedPositionScrollX = 0; | ||||||||||||||||||||
|
@@ -136,6 +156,7 @@ public void onInitializeAccessibilityNodeInfo( | |||||||||||||||||||
}); | ||||||||||||||||||||
|
||||||||||||||||||||
mScroller = getOverScrollerFromParent(); | ||||||||||||||||||||
setOnHierarchyChangeListener(this); | ||||||||||||||||||||
mLayoutDirection = | ||||||||||||||||||||
I18nUtil.getInstance().isRTL(context) | ||||||||||||||||||||
? ViewCompat.LAYOUT_DIRECTION_RTL | ||||||||||||||||||||
|
@@ -248,6 +269,14 @@ public void setOverflow(String overflow) { | |||||||||||||||||||
invalidate(); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
public void setMaintainVisibleContentPosition( | ||||||||||||||||||||
ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData) { | ||||||||||||||||||||
mMaintainVisibleContentPositionData = maintainVisibleContentPositionData; | ||||||||||||||||||||
if (maintainVisibleContentPositionData != null) { | ||||||||||||||||||||
computeFirstVisibleItemForMaintainVisibleContentPosition(); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
public @Nullable String getOverflow() { | ||||||||||||||||||||
return mOverflow; | ||||||||||||||||||||
|
@@ -436,6 +465,14 @@ protected void onScrollChanged(int x, int y, int oldX, int oldY) { | |||||||||||||||||||
mOnScrollDispatchHelper.getXFlingVelocity(), | ||||||||||||||||||||
mOnScrollDispatchHelper.getYFlingVelocity()); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if (mMaintainVisibleContentPositionData != null) { | ||||||||||||||||||||
// We don't want to compute the first visible view everytime onScrollChanged gets called (can | ||||||||||||||||||||
// be multiple times per second). | ||||||||||||||||||||
// The following logic debounces the computation by 100ms (arbitrary value). | ||||||||||||||||||||
mHandler.removeCallbacks(mComputeFirstVisibleViewRunnable); | ||||||||||||||||||||
mHandler.postDelayed(mComputeFirstVisibleViewRunnable, 100); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
|
@@ -1084,6 +1121,18 @@ public void setBorderStyle(@Nullable String style) { | |||||||||||||||||||
mReactBackgroundManager.setBorderStyle(style); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
public void onChildViewAdded(View parent, View child) { | ||||||||||||||||||||
mContentView = child; | ||||||||||||||||||||
mContentView.addOnLayoutChangeListener(this); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
public void onChildViewRemoved(View parent, View child) { | ||||||||||||||||||||
mContentView.removeOnLayoutChangeListener(this); | ||||||||||||||||||||
mContentView = null; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Calls `smoothScrollTo` and updates state. | ||||||||||||||||||||
* | ||||||||||||||||||||
|
@@ -1239,6 +1288,97 @@ private void updateStateOnScroll() { | |||||||||||||||||||
updateStateOnScroll(getScrollX(), getScrollY()); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Called when a mContentView's layout has changed. Fixes the scroll position depending on | ||||||||||||||||||||
* maintainVisibleContentPosition | ||||||||||||||||||||
*/ | ||||||||||||||||||||
@Override | ||||||||||||||||||||
public void onLayoutChange( | ||||||||||||||||||||
View v, | ||||||||||||||||||||
int left, | ||||||||||||||||||||
int top, | ||||||||||||||||||||
int right, | ||||||||||||||||||||
int bottom, | ||||||||||||||||||||
int oldLeft, | ||||||||||||||||||||
int oldTop, | ||||||||||||||||||||
int oldRight, | ||||||||||||||||||||
int oldBottom) { | ||||||||||||||||||||
if (mContentView == null) { | ||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if (this.mMaintainVisibleContentPositionData != null) { | ||||||||||||||||||||
scrollMaintainVisibleContentPosition(); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Called when maintainVisibleContentPosition is used and after a scroll. Finds the first | ||||||||||||||||||||
* completely visible view in the ScrollView and stores it for later use. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
private void computeFirstVisibleItemForMaintainVisibleContentPosition() { | ||||||||||||||||||||
ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = | ||||||||||||||||||||
mMaintainVisibleContentPositionData; | ||||||||||||||||||||
if (maintainVisibleContentPositionData == null) return; | ||||||||||||||||||||
|
||||||||||||||||||||
int currentScrollX = getScrollX(); | ||||||||||||||||||||
int minIdx = maintainVisibleContentPositionData.minIndexForVisible; | ||||||||||||||||||||
|
||||||||||||||||||||
ReactViewGroup contentView = (ReactViewGroup) getChildAt(0); | ||||||||||||||||||||
if (contentView == null) return; | ||||||||||||||||||||
|
||||||||||||||||||||
for (int i = minIdx; i < contentView.getChildCount(); i++) { | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to use binary search here to reduce computational complexity. Normally this is not a big deal, but here we are calling this method on every scroll ends (with the debounce it's better, but still will call this a lot). If we have many items in the list, this could lead to a slow perf issue. |
||||||||||||||||||||
// Find the first entirely visible view. This must be done after we update the content offset | ||||||||||||||||||||
// or it will tend to grab rows that were made visible by the shift in position | ||||||||||||||||||||
View child = contentView.getChildAt(i); | ||||||||||||||||||||
if (child.getX() >= currentScrollX || i == contentView.getChildCount() - 1) { | ||||||||||||||||||||
firstVisibleViewForMaintainVisibleContentPosition = new WeakReference<>(child); | ||||||||||||||||||||
Rect frame = new Rect(); | ||||||||||||||||||||
child.getHitRect(frame); | ||||||||||||||||||||
prevFirstVisibleFrameForMaintainVisibleContentPosition = frame; | ||||||||||||||||||||
break; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Called when maintainVisibleContentPosition is used and after a layout change. Detects if the | ||||||||||||||||||||
* layout change impacts the scroll position and corrects it if needed. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
private void scrollMaintainVisibleContentPosition() { | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are some problems with this approach.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I've been digging into this problem and trying to find a solution. Any suggestions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless I'm mistaken, the same problem exists with the current iOS implementation of |
||||||||||||||||||||
ReactScrollViewMaintainVisibleContentPositionData maintainVisibleContentPositionData = | ||||||||||||||||||||
this.mMaintainVisibleContentPositionData; | ||||||||||||||||||||
if (maintainVisibleContentPositionData == null) return; | ||||||||||||||||||||
|
||||||||||||||||||||
int currentScrollX = getScrollX(); | ||||||||||||||||||||
|
||||||||||||||||||||
View firstVisibleView = | ||||||||||||||||||||
firstVisibleViewForMaintainVisibleContentPosition != null | ||||||||||||||||||||
? firstVisibleViewForMaintainVisibleContentPosition.get() | ||||||||||||||||||||
: null; | ||||||||||||||||||||
if (firstVisibleView == null) return; | ||||||||||||||||||||
Comment on lines
+1355
to
+1359
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||
Rect prevFirstVisibleFrame = this.prevFirstVisibleFrameForMaintainVisibleContentPosition; | ||||||||||||||||||||
if (prevFirstVisibleFrame == null) return; | ||||||||||||||||||||
|
||||||||||||||||||||
Rect newFrame = new Rect(); | ||||||||||||||||||||
firstVisibleView.getHitRect(newFrame); | ||||||||||||||||||||
int deltaX = newFrame.left - prevFirstVisibleFrame.left; | ||||||||||||||||||||
|
||||||||||||||||||||
if (Math.abs(deltaX) > 1) { | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may be better to store this value using a constant.
Suggested change
|
||||||||||||||||||||
int scrollXTo = getScrollX() + deltaX; | ||||||||||||||||||||
|
||||||||||||||||||||
scrollTo(scrollXTo, getScrollY()); | ||||||||||||||||||||
|
||||||||||||||||||||
Integer autoScrollThreshold = maintainVisibleContentPositionData.autoScrollToTopThreshold; | ||||||||||||||||||||
if (autoScrollThreshold != null) { | ||||||||||||||||||||
// If the offset WAS within the threshold of the start, animate to the start. | ||||||||||||||||||||
if (currentScrollX - deltaX <= autoScrollThreshold) { | ||||||||||||||||||||
reactSmoothScrollTo(0, getScrollY()); | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
public FabricViewStateManager getFabricViewStateManager() { | ||||||||||||||||||||
return mFabricViewStateManager; | ||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you choose to use a
WeakReference
here?