-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
15546b6
commit 0b214f7
Showing
2 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java | ||
index fff761f..2cebd6b 100644 | ||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java | ||
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/MaintainVisibleScrollPositionHelper.java | ||
@@ -82,6 +82,7 @@ public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & | ||
return; | ||
} | ||
mListening = false; | ||
+ mFirstVisibleView = null; | ||
getUIManagerModule().removeUIManagerEventListener(this); | ||
} | ||
|
||
@@ -89,20 +90,19 @@ public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & | ||
* Update the scroll position of the managed ScrollView. This should be called after layout has | ||
* been updated. | ||
*/ | ||
- public void updateScrollPosition() { | ||
+ public void onLayout() { | ||
// On Fabric this will be called internally in `didMountItems`. | ||
- if (ViewUtil.getUIManagerType(mScrollView.getId()) == UIManagerType.FABRIC) { | ||
- return; | ||
+ if (ViewUtil.getUIManagerType(mScrollView.getId()) != UIManagerType.FABRIC) { | ||
+ didMountItemsInternal(); | ||
} | ||
- updateScrollPositionInternal(); | ||
} | ||
|
||
- private void updateScrollPositionInternal() { | ||
- if (mConfig == null || mFirstVisibleView == null || mPrevFirstVisibleFrame == null) { | ||
+ private void didMountItemsInternal() { | ||
+ if (mConfig == null || mPrevFirstVisibleFrame == null) { | ||
return; | ||
} | ||
|
||
- View firstVisibleView = mFirstVisibleView.get(); | ||
+ View firstVisibleView = getFirstVisibleView(); | ||
if (firstVisibleView == null) { | ||
return; | ||
} | ||
@@ -114,7 +114,7 @@ public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & | ||
int deltaX = newFrame.left - mPrevFirstVisibleFrame.left; | ||
if (deltaX != 0) { | ||
int scrollX = mScrollView.getScrollX(); | ||
- mScrollView.scrollTo(scrollX + deltaX, mScrollView.getScrollY()); | ||
+ mScrollView.scrollToPreservingMomentum(scrollX + deltaX, mScrollView.getScrollY()); | ||
mPrevFirstVisibleFrame = newFrame; | ||
if (mConfig.autoScrollToTopThreshold != null | ||
&& scrollX <= mConfig.autoScrollToTopThreshold) { | ||
@@ -125,7 +125,7 @@ public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & | ||
int deltaY = newFrame.top - mPrevFirstVisibleFrame.top; | ||
if (deltaY != 0) { | ||
int scrollY = mScrollView.getScrollY(); | ||
- mScrollView.scrollTo(mScrollView.getScrollX(), scrollY + deltaY); | ||
+ mScrollView.scrollToPreservingMomentum(mScrollView.getScrollX(), scrollY + deltaY); | ||
mPrevFirstVisibleFrame = newFrame; | ||
if (mConfig.autoScrollToTopThreshold != null | ||
&& scrollY <= mConfig.autoScrollToTopThreshold) { | ||
@@ -146,7 +146,7 @@ public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & | ||
ViewUtil.getUIManagerType(mScrollView.getId()))); | ||
} | ||
|
||
- private void computeTargetView() { | ||
+ public void onScroll() { | ||
if (mConfig == null) { | ||
return; | ||
} | ||
@@ -156,17 +156,45 @@ public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & | ||
} | ||
|
||
int currentScroll = mHorizontal ? mScrollView.getScrollX() : mScrollView.getScrollY(); | ||
+ View firstVisibleView = null; | ||
+ // We cannot assume that the views will be in position order because of things like z-index | ||
+ // which will change the order of views in their parent. This means we need to iterate through | ||
+ // the full children array and find the view with the smallest position that is bigger than | ||
+ // the scroll position. | ||
+ float firstVisibleViewPosition = Float.MAX_VALUE; | ||
for (int i = mConfig.minIndexForVisible; i < contentView.getChildCount(); i++) { | ||
View child = contentView.getChildAt(i); | ||
float position = mHorizontal ? child.getX() : child.getY(); | ||
- if (position > currentScroll || i == contentView.getChildCount() - 1) { | ||
- mFirstVisibleView = new WeakReference<>(child); | ||
- Rect frame = new Rect(); | ||
- child.getHitRect(frame); | ||
- mPrevFirstVisibleFrame = frame; | ||
- break; | ||
+ if ((position > currentScroll && position < firstVisibleViewPosition) || | ||
+ (firstVisibleView == null && i == contentView.getChildCount() - 1)) { | ||
+ firstVisibleView = child; | ||
+ firstVisibleViewPosition = position; | ||
+ } | ||
+ } | ||
+ mFirstVisibleView = new WeakReference<>(firstVisibleView); | ||
+ } | ||
+ | ||
+ private View getFirstVisibleView() { | ||
+ return mFirstVisibleView != null ? mFirstVisibleView.get() : null; | ||
+ } | ||
+ | ||
+ private void willMountItemsInternal() { | ||
+ View firstVisibleView = getFirstVisibleView(); | ||
+ | ||
+ // If we don't have a first visible view because no scroll happened call onScroll | ||
+ // to update it. | ||
+ if (firstVisibleView == null) { | ||
+ onScroll(); | ||
+ firstVisibleView = getFirstVisibleView(); | ||
+ | ||
+ // There are cases where it is possible for this to still be null so just bail out. | ||
+ if (firstVisibleView == null) { | ||
+ return; | ||
} | ||
} | ||
+ Rect frame = new Rect(); | ||
+ firstVisibleView.getHitRect(frame); | ||
+ mPrevFirstVisibleFrame = frame; | ||
} | ||
|
||
// UIManagerListener | ||
@@ -177,19 +205,19 @@ public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & | ||
new Runnable() { | ||
@Override | ||
public void run() { | ||
- computeTargetView(); | ||
+ willMountItemsInternal(); | ||
} | ||
}); | ||
} | ||
|
||
@Override | ||
public void willMountItems(UIManager uiManager) { | ||
- computeTargetView(); | ||
+ willMountItemsInternal(); | ||
} | ||
|
||
@Override | ||
public void didMountItems(UIManager uiManager) { | ||
- updateScrollPositionInternal(); | ||
+ didMountItemsInternal(); | ||
} | ||
|
||
@Override | ||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java | ||
index 7dbcb78..34ad4a1 100644 | ||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java | ||
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java | ||
@@ -475,6 +475,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView | ||
mOnScrollDispatchHelper.getXFlingVelocity(), | ||
mOnScrollDispatchHelper.getYFlingVelocity()); | ||
} | ||
+ | ||
+ if (mMaintainVisibleContentPositionHelper != null) { | ||
+ mMaintainVisibleContentPositionHelper.onScroll(); | ||
+ } | ||
} | ||
|
||
@Nullable | ||
@@ -1323,6 +1327,14 @@ public class ReactHorizontalScrollView extends HorizontalScrollView | ||
setPendingContentOffsets(x, y); | ||
} | ||
|
||
+ /** | ||
+ * Scrolls to a new position preserving any momentum scrolling animation. | ||
+ */ | ||
+ public void scrollToPreservingMomentum(int x, int y) { | ||
+ scrollTo(x, y); | ||
+ recreateFlingAnimation(x, Integer.MAX_VALUE); | ||
+ } | ||
+ | ||
private boolean isContentReady() { | ||
View child = getContentView(); | ||
return child != null && child.getWidth() != 0 && child.getHeight() != 0; | ||
@@ -1372,28 +1384,25 @@ public class ReactHorizontalScrollView extends HorizontalScrollView | ||
if (mReactScrollViewScrollState.getLayoutDirection() == LAYOUT_DIRECTION_RTL) { | ||
adjustPositionForContentChangeRTL(left, right, oldLeft, oldRight); | ||
} else if (mMaintainVisibleContentPositionHelper != null) { | ||
- mMaintainVisibleContentPositionHelper.updateScrollPosition(); | ||
+ mMaintainVisibleContentPositionHelper.onLayout(); | ||
} | ||
} | ||
|
||
- private void adjustPositionForContentChangeRTL(int left, int right, int oldLeft, int oldRight) { | ||
- // If we have any pending custon flings (e.g. from aninmated `scrollTo`, or flinging to a snap | ||
- // point), finish them, commiting the final `scrollX`. | ||
+ /** | ||
+ * If we are in the middle of a fling animation from the user removing their finger | ||
+ * (OverScroller is in `FLING_MODE`), recreate the existing fling animation since it was | ||
+ * calculated against outdated scroll offsets. | ||
+ */ | ||
+ private void recreateFlingAnimation(int scrollX, int maxX) { | ||
+ // If we have any pending custom flings (e.g. from animated `scrollTo`, or flinging to a snap | ||
+ // point), cancel them. | ||
// TODO: Can we be more graceful (like OverScroller flings)? | ||
if (getFlingAnimator().isRunning()) { | ||
- getFlingAnimator().end(); | ||
+ getFlingAnimator().cancel(); | ||
} | ||
|
||
- int distanceToRightEdge = oldRight - getScrollX(); | ||
- int newWidth = right - left; | ||
- int scrollX = newWidth - distanceToRightEdge; | ||
- scrollTo(scrollX, getScrollY()); | ||
- | ||
- // If we are in the middle of a fling animation from the user removing their finger | ||
- // (OverScroller is in `FLING_MODE`), we must cancel and recreate the existing fling animation | ||
- // since it was calculated against outdated scroll offsets. | ||
if (mScroller != null && !mScroller.isFinished()) { | ||
- // Calculate the veliocity and position of the fling animation at the time of this layout | ||
+ // Calculate the velocity and position of the fling animation at the time of this layout | ||
// event, which may be later than the last ScrollView tick. These values are not commited to | ||
// the underlying ScrollView, which will recalculate positions on its next tick. | ||
int scrollerXBeforeTick = mScroller.getCurrX(); | ||
@@ -1413,13 +1422,29 @@ public class ReactHorizontalScrollView extends HorizontalScrollView | ||
float flingVelocityX = mScroller.getCurrVelocity() * direction; | ||
|
||
mScroller.fling( | ||
- scrollX, getScrollY(), (int) flingVelocityX, 0, 0, newWidth - getWidth(), 0, 0); | ||
+ scrollX, getScrollY(), (int) flingVelocityX, 0, 0, maxX, 0, 0); | ||
} else { | ||
scrollTo(scrollX + (mScroller.getCurrX() - scrollerXBeforeTick), getScrollY()); | ||
} | ||
} | ||
} | ||
|
||
+ private void adjustPositionForContentChangeRTL(int left, int right, int oldLeft, int oldRight) { | ||
+ // If we have any pending custom flings (e.g. from animated `scrollTo`, or flinging to a snap | ||
+ // point), finish them, committing the final `scrollX`. | ||
+ // TODO: Can we be more graceful (like OverScroller flings)? | ||
+ if (getFlingAnimator().isRunning()) { | ||
+ getFlingAnimator().end(); | ||
+ } | ||
+ | ||
+ int distanceToRightEdge = oldRight - getScrollX(); | ||
+ int newWidth = right - left; | ||
+ int scrollX = newWidth - distanceToRightEdge; | ||
+ scrollTo(scrollX, getScrollY()); | ||
+ | ||
+ recreateFlingAnimation(scrollX, newWidth - getWidth()); | ||
+ } | ||
+ | ||
@Nullable | ||
public StateWrapper getStateWrapper() { | ||
return mStateWrapper; | ||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java | ||
index 16521c8..f966e4a 100644 | ||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java | ||
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java | ||
@@ -392,6 +392,10 @@ public class ReactScrollView extends ScrollView | ||
mOnScrollDispatchHelper.getXFlingVelocity(), | ||
mOnScrollDispatchHelper.getYFlingVelocity()); | ||
} | ||
+ | ||
+ if (mMaintainVisibleContentPositionHelper != null) { | ||
+ mMaintainVisibleContentPositionHelper.onScroll(); | ||
+ } | ||
} | ||
|
||
@Override | ||
@@ -1091,6 +1095,55 @@ public class ReactScrollView extends ScrollView | ||
setPendingContentOffsets(x, y); | ||
} | ||
|
||
+ /** | ||
+ * If we are in the middle of a fling animation from the user removing their finger | ||
+ * (OverScroller is in `FLING_MODE`), recreate the existing fling animation since it was | ||
+ * calculated against outdated scroll offsets. | ||
+ */ | ||
+ private void recreateFlingAnimation(int scrollY) { | ||
+ // If we have any pending custom flings (e.g. from animated `scrollTo`, or flinging to a snap | ||
+ // point), cancel them. | ||
+ // TODO: Can we be more graceful (like OverScroller flings)? | ||
+ if (getFlingAnimator().isRunning()) { | ||
+ getFlingAnimator().cancel(); | ||
+ } | ||
+ | ||
+ if (mScroller != null && !mScroller.isFinished()) { | ||
+ // Calculate the velocity and position of the fling animation at the time of this layout | ||
+ // event, which may be later than the last ScrollView tick. These values are not committed to | ||
+ // the underlying ScrollView, which will recalculate positions on its next tick. | ||
+ int scrollerYBeforeTick = mScroller.getCurrY(); | ||
+ boolean hasMoreTicks = mScroller.computeScrollOffset(); | ||
+ | ||
+ // Stop the existing animation at the current state of the scroller. We will then recreate | ||
+ // it starting at the adjusted y offset. | ||
+ mScroller.forceFinished(true); | ||
+ | ||
+ if (hasMoreTicks) { | ||
+ // OverScroller.getCurrVelocity() returns an absolute value of the velocity a current fling | ||
+ // animation (only FLING_MODE animations). We derive direction along the Y axis from the | ||
+ // start and end of the, animation assuming ScrollView never fires horizontal fling | ||
+ // animations. | ||
+ // TODO: This does not fully handle overscroll. | ||
+ float direction = Math.signum(mScroller.getFinalY() - mScroller.getStartY()); | ||
+ float flingVelocityY = mScroller.getCurrVelocity() * direction; | ||
+ | ||
+ mScroller.fling( | ||
+ getScrollX(), scrollY, 0, (int) flingVelocityY, 0, 0, 0, Integer.MAX_VALUE); | ||
+ } else { | ||
+ scrollTo(getScrollX(), scrollY + (mScroller.getCurrX() - scrollerYBeforeTick)); | ||
+ } | ||
+ } | ||
+ } | ||
+ | ||
+ /** | ||
+ * Scrolls to a new position preserving any momentum scrolling animation. | ||
+ */ | ||
+ public void scrollToPreservingMomentum(int x, int y) { | ||
+ scrollTo(x, y); | ||
+ recreateFlingAnimation(y); | ||
+ } | ||
+ | ||
private boolean isContentReady() { | ||
View child = getContentView(); | ||
return child != null && child.getWidth() != 0 && child.getHeight() != 0; | ||
@@ -1134,7 +1187,7 @@ public class ReactScrollView extends ScrollView | ||
} | ||
|
||
if (mMaintainVisibleContentPositionHelper != null) { | ||
- mMaintainVisibleContentPositionHelper.updateScrollPosition(); | ||
+ mMaintainVisibleContentPositionHelper.onLayout(); | ||
} | ||
|
||
if (isShown() && isContentReady()) { | ||
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java | ||
index 49fe5f5..c87c82d 100644 | ||
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java | ||
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.java | ||
@@ -595,5 +595,7 @@ public class ReactScrollViewHelper { | ||
|
||
public interface HasSmoothScroll { | ||
void reactSmoothScrollTo(int x, int y); | ||
+ | ||
+ void scrollToPreservingMomentum(int x, int y); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters