-
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
Conversation
Base commit: 9b4f8e0 |
Base commit: 9b4f8e0 |
Here is an alternative native module implementation, for those who need package com.yourapp;
import android.view.View;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.ReactShadowNode;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIImplementation;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.UIManagerModuleListener;
import com.facebook.react.views.scroll.ReactScrollView;
import com.facebook.react.views.view.ReactViewGroup;
import java.util.HashMap;
public class ScrollViewMagicModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
private HashMap<Integer, UIManagerModuleListener> uiManagerModuleListeners;
public ScrollViewMagicModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@Override
public String getName() {
return "ScrollViewMagic";
}
@Override
public void initialize() {
super.initialize();
this.uiManagerModuleListeners = new HashMap<>();
}
@ReactMethod
public void enableMaintainVisibleContentPosition(final int viewTag, final Promise promise) {
final UIManagerModule uiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
this.reactContext.runOnUiQueueThread(new Runnable() {
@Override
public void run() {
try {
final ReactScrollView scrollView = (ReactScrollView)uiManagerModule.resolveView(viewTag);
final UIManagerModuleListener uiManagerModuleListener = new UIManagerModuleListener() {
private int minIndexForVisible = 0;
private int prevFirstVisibleTop = 0;
private View firstVisibleView = null;
@Override
public void willDispatchViewUpdates(final UIManagerModule uiManagerModule) {
uiManagerModule.prependUIBlock(new UIBlock() {
@Override
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
ReactViewGroup mContentView = (ReactViewGroup)scrollView.getChildAt(0);
if (mContentView == null) return;
for (int ii = minIndexForVisible; ii < mContentView.getChildCount(); ++ii) {
View subview = mContentView.getChildAt(ii);
if (subview.getTop() >= scrollView.getScrollY()) {
prevFirstVisibleTop = subview.getTop();
firstVisibleView = subview;
break;
}
}
}
});
UIImplementation.LayoutUpdateListener layoutUpdateListener = new UIImplementation.LayoutUpdateListener() {
@Override
public void onLayoutUpdated(ReactShadowNode root) {
if (firstVisibleView == null) return;
int deltaY = firstVisibleView.getTop() - prevFirstVisibleTop;
if (Math.abs(deltaY) > 0) {
scrollView.setScrollY(scrollView.getScrollY() + deltaY);
}
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
}
};
uiManagerModule.getUIImplementation().setLayoutUpdateListener(layoutUpdateListener);
}
};
uiManagerModule.addUIManagerListener(uiManagerModuleListener);
int key = uiManagerModuleListeners.size() + 1;
uiManagerModuleListeners.put(key, uiManagerModuleListener);
promise.resolve(key);
} catch(IllegalViewOperationException e) {
promise.resolve(-1);
}
}
});
}
@ReactMethod
public void disableMaintainVisibleContentPosition(int key, Promise promise) {
if (key >= 0) {
final UIManagerModule uiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
uiManagerModule.removeUIManagerListener(uiManagerModuleListeners.remove(key));
}
promise.resolve(null);
}
} JS side: const nativeModule: {
enableMaintainVisibleContentPosition(viewTag: number): Promise<number>;
disableMaintainVisibleContentPosition(handle: number): Promise<void>;
} = NativeModules.ScrollViewMagic; // `ref` is the ref to your ScrollView / FlatList
useEffect(() => {
let cleanupPromise: Promise<number> | undefined;
if (Constants.isAndroid) {
const viewTag = findNodeHandle(ref.current);
cleanupPromise = nativeModule.enableMaintainVisibleContentPosition(
viewTag
);
}
return () => {
void cleanupPromise?.then((handle) => {
void nativeModule.disableMaintainVisibleContentPosition(handle);
});
};
}, [ref]); |
Hi @chrisglein I saw you roaming around issue #29055 that this PR aims to fix. Since you are a contributor on this repo do you know if there are any next steps I'm missing to get this PR merged? It's been opened for a bit more than a month now and we'd like to have this scroll improvement for our app. |
I'm mainly acting as an issue first responder, helping logged issues have the best chance of success. I've tagged the issue for this to see if that helps it get attention. You may also want to ping the discord to see if you can get some eyes on this. |
Thank you Chris! I joined the Discord 🤞 |
Hey all, I'm going to find some time working on this, hopefully this month. Sorry for the delay |
Any update ? |
@maxoumime any update? |
@maxoumime +1 |
1 similar comment
@maxoumime +1 |
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; |
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?
any updates ? |
any update ? |
Any updates?? |
@janicduplessis and I have a working version of this feature completed in the Expensify fork of react-native, and are just working on more battle-testing of the feature before submitting PR(s) to the upstream repo. This PR was definitely a helpful start, but ultimately the implementation we landed on is this. We've also implemented Disclaimer: Unfortunately it does not seem to work with Fabric enabled though (nor does the existing iOS implementation, AFAICT) 😞 It's unclear that a native implementation of this prop will be needed in a Fabric world, however. My guess is probably not. Note: The latest version of the Expensify RN fork is |
…kBack). The logic was moved to ReactScrollView onLayoutChange. Try to import some of the functionalities from PR facebook#29466 Use maintainVisibleContentPosition instead of existing functionality in FlatList implementation PR 29466 facebook#29466
Use maintainVisibleContentPosition instead of existing functionality in FlatList implementation PR 29466 facebook#29466
Any plan to upstream your efforts? |
Yes, as stated above we want to production-test the features before attempting to upstream them. You can help out in that effort by using the Expensify fork of React Native in the meantime, which is available on npm just like the normal React Native repo. If you do encounter issues specific to these new features, you can open an issue in our fork and we'll do our best to address them. Edit: I realized that you can't create issues in our fork, so if you need to get our attention the best way will probably be to join our open-source slack |
@janicduplessis opened a new PR which implements |
This PR is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days. |
Summary
This PR adds the support for ScrollView's
maintainVisibleContentPosition
property to Android. This property is currently only available on iOS and is especially useful for chat-like scrollviews, where you want the scroll position to stick after layout changes.Fixes #29055
This PR will impact the documentation, so I opened a draft PR on facebook/react-native-website#2088.
Changelog
[Android] [Added] -
ScrollView.maintainVisibleContentPosition
. This property is not iOS-only anymore.Test Plan
Most of the new code is based on the iOS code. The implementation differs a bit but is working great. You can try it out on the RNTester app, I added a new example called
ScrollViewExpandingExample
, available both on Android and iOS. GIFs below.