Skip to content

Commit

Permalink
Fix mvcp on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
janicduplessis committed Aug 1, 2024
1 parent 15546b6 commit 0b214f7
Show file tree
Hide file tree
Showing 2 changed files with 335 additions and 0 deletions.
334 changes: 334 additions & 0 deletions patches/react-native+0.73.4+024+fixMVCPAndroid.patch
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);
}
}
1 change: 1 addition & 0 deletions src/pages/home/report/ReportActionsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ function ReportActionsView({
if (!reportActions.length) {
return null;
}

// AutoScroll is disabled when we do linking to a specific reportAction
const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isNavigatingToLinkedMessage);
return (
Expand Down

0 comments on commit 0b214f7

Please sign in to comment.