Skip to content

Commit

Permalink
Add maintainVisibleContentPosition prop for android scroll view
Browse files Browse the repository at this point in the history
  • Loading branch information
roryabraham authored and janicduplessis committed Oct 22, 2022
1 parent 950ea91 commit c069ae3
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 11 deletions.
1 change: 0 additions & 1 deletion Libraries/Components/ScrollView/ScrollView.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ type IOSProps = $ReadOnly<{|
* visibility. Occlusion, transforms, and other complexity won't be taken into account as to
* whether content is "visible" or not.
*
* @platform ios
*/
maintainVisibleContentPosition?: ?$ReadOnly<{|
minIndexForVisible: number,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.scroll;

import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.Nullable;

import com.facebook.infer.annotation.Assertions;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.bridge.UIManagerListener;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.common.ViewUtil;
import com.facebook.react.views.view.ReactViewGroup;
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasSmoothScroll;

import java.lang.ref.WeakReference;

/**
* Manage state for the maintainVisibleContentPosition prop.
*
* This uses UIManager to listen to updates and capture position of items before and after layout.
*/
public class MaintainVisibleScrollPositionHelper<ScrollViewT extends ViewGroup & HasSmoothScroll> implements UIManagerListener {
private final ScrollViewT mScrollView;
private final boolean mHorizontal;
private @Nullable Config mConfig;
private @Nullable WeakReference<View> mFirstVisibleView = null;
private @Nullable Rect mPrevFirstVisibleFrame = null;
private boolean mListening = false;

public static class Config {
public final int minIndexForVisible;
public final @Nullable Integer autoScrollToTopThreshold;

Config(int minIndexForVisible, @Nullable Integer autoScrollToTopThreshold) {
this.minIndexForVisible = minIndexForVisible;
this.autoScrollToTopThreshold = autoScrollToTopThreshold;
}

static Config fromReadableMap(ReadableMap value) {
int minIndexForVisible = value.getInt("minIndexForVisible");
Integer autoScrollToTopThreshold =
value.hasKey("autoscrollToTopThreshold")
? value.getInt("autoscrollToTopThreshold")
: null;
return new Config(minIndexForVisible, autoScrollToTopThreshold);
}
}

public MaintainVisibleScrollPositionHelper(ScrollViewT scrollView, boolean horizontal) {
mScrollView = scrollView;
mHorizontal = horizontal;
}

public void setConfig(@Nullable Config config) {
mConfig = config;
}

/**
* Start listening to view hierarchy updates. Should be called when this is created.
*/
public void start() {
if (mListening) {
return;
}
mListening = true;
getUIManagerModule().addUIManagerEventListener(this);
}

/**
* Stop listening to view hierarchy updates. Should be called before this is destroyed.
*/
public void stop() {
if (!mListening) {
return;
}
mListening = false;
getUIManagerModule().removeUIManagerEventListener(this);
}

/**
* Update the scroll position of the managed ScrollView. This should be called after layout
* has been updated.
*/
public void updateScrollPosition() {
if (mConfig == null
|| mFirstVisibleView == null
|| mPrevFirstVisibleFrame == null) {
return;
}

View firstVisibleView = mFirstVisibleView.get();
Rect newFrame = new Rect();
firstVisibleView.getHitRect(newFrame);

if (mHorizontal) {
int deltaX = newFrame.left - mPrevFirstVisibleFrame.left;
if (deltaX != 0) {
int scrollX = mScrollView.getScrollX();
mScrollView.scrollTo(scrollX + deltaX, mScrollView.getScrollY());
mPrevFirstVisibleFrame = newFrame;
if (mConfig.autoScrollToTopThreshold != null && scrollX <= mConfig.autoScrollToTopThreshold) {
mScrollView.reactSmoothScrollTo(0, mScrollView.getScrollY());
}
}
} else {
int deltaY = newFrame.top - mPrevFirstVisibleFrame.top;
if (deltaY != 0) {
int scrollY = mScrollView.getScrollY();
mScrollView.scrollTo(mScrollView.getScrollX(), scrollY + deltaY);
mPrevFirstVisibleFrame = newFrame;
if (mConfig.autoScrollToTopThreshold != null && scrollY <= mConfig.autoScrollToTopThreshold) {
mScrollView.reactSmoothScrollTo(mScrollView.getScrollX(), 0);
}
}
}
}

private @Nullable ReactViewGroup getContentView() {
return (ReactViewGroup) mScrollView.getChildAt(0);
}

private UIManager getUIManagerModule() {
return Assertions.assertNotNull(
UIManagerHelper.getUIManager(
(ReactContext) mScrollView.getContext(),
ViewUtil.getUIManagerType(mScrollView.getId())));
}

private void computeTargetView() {
if (mConfig == null) {
return;
}
ReactViewGroup contentView = getContentView();
if (contentView == null) {
return;
}

int currentScroll = mHorizontal ? mScrollView.getScrollX() : mScrollView.getScrollY();
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;
}
}
}

// UIManagerListener

@Override
public void willDispatchViewUpdates(final UIManager uiManager) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
computeTargetView();
}
});
}

@Override
public void didDispatchMountItems(UIManager uiManager) {
// noop
}

@Override
public void didScheduleMountItems(UIManager uiManager) {
// noop
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasFlingAnimator;
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasScrollEventThrottle;
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasScrollState;
import com.facebook.react.views.scroll.ReactScrollViewHelper.HasSmoothScroll;
import com.facebook.react.views.scroll.ReactScrollViewHelper.ReactScrollViewScrollState;
import com.facebook.react.views.view.ReactViewBackgroundManager;
import java.lang.reflect.Field;
Expand All @@ -54,11 +55,14 @@
/** Similar to {@link ReactScrollView} but only supports horizontal scrolling. */
public class ReactHorizontalScrollView extends HorizontalScrollView
implements ReactClippingViewGroup,
ViewGroup.OnHierarchyChangeListener,
View.OnLayoutChangeListener,
FabricViewStateManager.HasFabricViewStateManager,
ReactOverflowViewWithInset,
HasScrollState,
HasFlingAnimator,
HasScrollEventThrottle {
HasScrollEventThrottle,
HasSmoothScroll {

private static boolean DEBUG_MODE = false && ReactBuildConfig.DEBUG;
private static String TAG = ReactHorizontalScrollView.class.getSimpleName();
Expand Down Expand Up @@ -107,6 +111,8 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
private PointerEvents mPointerEvents = PointerEvents.AUTO;
private long mLastScrollDispatchTime = 0;
private int mScrollEventThrottle = 0;
private @Nullable View mContentView;
private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper = null;

private final Rect mTempRect = new Rect();

Expand All @@ -127,6 +133,8 @@ public ReactHorizontalScrollView(Context context, @Nullable FpsListener fpsListe
I18nUtil.getInstance().isRTL(context)
? ViewCompat.LAYOUT_DIRECTION_RTL
: ViewCompat.LAYOUT_DIRECTION_LTR);

setOnHierarchyChangeListener(this);
}

public boolean getScrollEnabled() {
Expand Down Expand Up @@ -243,6 +251,19 @@ public void setOverflow(String overflow) {
invalidate();
}

public void setMaintainVisibleContentPosition(@Nullable MaintainVisibleScrollPositionHelper.Config config) {
if (config != null && mMaintainVisibleContentPositionHelper == null) {
mMaintainVisibleContentPositionHelper = new MaintainVisibleScrollPositionHelper(this, true);
mMaintainVisibleContentPositionHelper.start();
} else if (config == null && mMaintainVisibleContentPositionHelper != null) {
mMaintainVisibleContentPositionHelper.stop();
mMaintainVisibleContentPositionHelper = null;
}
if (mMaintainVisibleContentPositionHelper != null) {
mMaintainVisibleContentPositionHelper.setConfig(config);
}
}

@Override
public @Nullable String getOverflow() {
return mOverflow;
Expand Down Expand Up @@ -635,6 +656,17 @@ protected void onAttachedToWindow() {
if (mRemoveClippedSubviews) {
updateClippingRect();
}
if (mMaintainVisibleContentPositionHelper != null) {
mMaintainVisibleContentPositionHelper.start();
}
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mMaintainVisibleContentPositionHelper != null) {
mMaintainVisibleContentPositionHelper.stop();
}
}

@Override
Expand Down Expand Up @@ -714,6 +746,18 @@ protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolea
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}

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

private void enableFpsListener() {
if (isScrollPerfLoggingEnabled()) {
Assertions.assertNotNull(mFpsListener);
Expand Down Expand Up @@ -1237,6 +1281,26 @@ private void setPendingContentOffsets(int x, int y) {
}
}

@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 (mMaintainVisibleContentPositionHelper != null) {
mMaintainVisibleContentPositionHelper.updateScrollPosition();
}
}

@Override
public FabricViewStateManager getFabricViewStateManager() {
return mFabricViewStateManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,16 @@ public void setContentOffset(ReactHorizontalScrollView view, ReadableMap value)
}
}

@ReactProp(name = "maintainVisibleContentPosition")
public void setMaintainVisibleContentPosition(ReactHorizontalScrollView view, ReadableMap value) {
if (value != null) {
view.setMaintainVisibleContentPosition(
MaintainVisibleScrollPositionHelper.Config.fromReadableMap(value));
} else {
view.setMaintainVisibleContentPosition(null);
}
}

@ReactProp(name = ViewProps.POINTER_EVENTS)
public void setPointerEvents(ReactHorizontalScrollView view, @Nullable String pointerEventsStr) {
view.setPointerEvents(PointerEvents.parsePointerEvents(pointerEventsStr));
Expand Down
Loading

0 comments on commit c069ae3

Please sign in to comment.