Skip to content

Support for ScrollView.maintainVisibleContentPosition on Android - #29466

Closed
maxbth wants to merge 28 commits into
react:mainfrom
maxbth:master
Closed

Support for ScrollView.maintainVisibleContentPosition on Android#29466
maxbth wants to merge 28 commits into
react:mainfrom
maxbth:master

Conversation

@maxbth

@maxbthmaxbth commented Jul 22, 2020

Copy link
Copy Markdown
Contributor

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 react/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.

react-horizontal-compressed
react-vertical-compressed

@facebook-github-botfacebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 22, 2020
@react-native-botreact-native-bot added Platform: Android Android applications. Type: Enhancement A new feature or enhancement of an existing feature. labels Jul 22, 2020
@analysis-bot

analysis-bot commented Jul 22, 2020

Copy link
Copy Markdown
PlatformEngineArchSize (bytes)Diff
androidhermesarm64-v8a9,196,911+6,442
androidhermesarmeabi-v7a8,722,544+6,440
androidhermesx869,638,946+6,443
androidhermesx86_649,606,540+6,448
androidjscarm64-v8a10,832,618+6,166
androidjscarmeabi-v7a9,749,519+6,170
androidjscx8610,870,053+6,173
androidjscx86_6411,479,152+6,171

Base commit: 9b4f8e0

@analysis-bot

analysis-bot commented Jul 22, 2020

Copy link
Copy Markdown
PlatformEngineArchSize (bytes)Diff
ios-universaln/a--

Base commit: 9b4f8e0

@stackia

stackia commented Jul 27, 2020

Copy link
Copy Markdown

Here is an alternative native module implementation, for those who need maintainVisibleContentPosition on Android now but don't want to maintain your own RN fork.

packagecom.yourapp;
importandroid.view.View;
importcom.facebook.react.bridge.Promise;
importcom.facebook.react.bridge.ReactApplicationContext;
importcom.facebook.react.bridge.ReactContextBaseJavaModule;
importcom.facebook.react.bridge.ReactMethod;
importcom.facebook.react.uimanager.IllegalViewOperationException;
importcom.facebook.react.uimanager.NativeViewHierarchyManager;
importcom.facebook.react.uimanager.ReactShadowNode;
importcom.facebook.react.uimanager.UIBlock;
importcom.facebook.react.uimanager.UIImplementation;
importcom.facebook.react.uimanager.UIManagerModule;
importcom.facebook.react.uimanager.UIManagerModuleListener;
importcom.facebook.react.views.scroll.ReactScrollView;
importcom.facebook.react.views.view.ReactViewGroup;
importjava.util.HashMap;
publicclassScrollViewMagicModuleextendsReactContextBaseJavaModule {
privatefinalReactApplicationContextreactContext;
privateHashMap<Integer, UIManagerModuleListener> uiManagerModuleListeners;
publicScrollViewMagicModule(ReactApplicationContextreactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@OverridepublicStringgetName() {
return"ScrollViewMagic";
}
@Overridepublicvoidinitialize() {
super.initialize();
this.uiManagerModuleListeners = newHashMap<>();
}
@ReactMethodpublicvoidenableMaintainVisibleContentPosition(finalintviewTag, finalPromisepromise) {
finalUIManagerModuleuiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
this.reactContext.runOnUiQueueThread(newRunnable() {
@Overridepublicvoidrun() {
try {
finalReactScrollViewscrollView = (ReactScrollView)uiManagerModule.resolveView(viewTag);
finalUIManagerModuleListeneruiManagerModuleListener = newUIManagerModuleListener() {
privateintminIndexForVisible = 0;
privateintprevFirstVisibleTop = 0;
privateViewfirstVisibleView = null;
@OverridepublicvoidwillDispatchViewUpdates(finalUIManagerModuleuiManagerModule) {
uiManagerModule.prependUIBlock(newUIBlock() {
@Overridepublicvoidexecute(NativeViewHierarchyManagernativeViewHierarchyManager) {
ReactViewGroupmContentView = (ReactViewGroup)scrollView.getChildAt(0);
if (mContentView == null) return;
for (intii = minIndexForVisible; ii < mContentView.getChildCount(); ++ii) {
Viewsubview = mContentView.getChildAt(ii);
if (subview.getTop() >= scrollView.getScrollY()) {
prevFirstVisibleTop = subview.getTop();
firstVisibleView = subview;
break;
}
}
}
});
UIImplementation.LayoutUpdateListenerlayoutUpdateListener = newUIImplementation.LayoutUpdateListener() {
@OverridepublicvoidonLayoutUpdated(ReactShadowNoderoot) {
if (firstVisibleView == null) return;
intdeltaY = firstVisibleView.getTop() - prevFirstVisibleTop;
if (Math.abs(deltaY) > 0) {
scrollView.setScrollY(scrollView.getScrollY() + deltaY);
}
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
}
};
uiManagerModule.getUIImplementation().setLayoutUpdateListener(layoutUpdateListener);
}
};
uiManagerModule.addUIManagerListener(uiManagerModuleListener);
intkey = uiManagerModuleListeners.size() + 1;
uiManagerModuleListeners.put(key, uiManagerModuleListener);
promise.resolve(key);
} catch(IllegalViewOperationExceptione) {
promise.resolve(-1);
}
}
});
}
@ReactMethodpublicvoiddisableMaintainVisibleContentPosition(intkey, Promisepromise) {
if (key >= 0) {
finalUIManagerModuleuiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
uiManagerModule.removeUIManagerListener(uiManagerModuleListeners.remove(key));
}
promise.resolve(null);
}
}

JS side:

constnativeModule: {enableMaintainVisibleContentPosition(viewTag: number): Promise<number>;disableMaintainVisibleContentPosition(handle: number): Promise<void>;}=NativeModules.ScrollViewMagic;
// `ref` is the ref to your ScrollView / FlatListuseEffect(()=>{letcleanupPromise: Promise<number>|undefined;if(Constants.isAndroid){constviewTag=findNodeHandle(ref.current);cleanupPromise=nativeModule.enableMaintainVisibleContentPosition(viewTag);}return()=>{voidcleanupPromise?.then((handle)=>{voidnativeModule.disableMaintainVisibleContentPosition(handle);});};},[ref]);

@maxbth

Copy link
Copy Markdown
ContributorAuthor

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.

@chrisglein

Copy link
Copy Markdown

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.

@maxbth

Copy link
Copy Markdown
ContributorAuthor

Thank you Chris! I joined the Discord 🤞

@ALexandreM75013

Copy link
Copy Markdown

Any update ?

@numandev1

Copy link
Copy Markdown
Contributor

@maxoumime any update?

@bigmoveus

bigmoveus commented Jun 12, 2022

Copy link
Copy Markdown

@maxoumime +1

1 similar comment
@andresribeiro

Copy link
Copy Markdown

@maxoumime +1

private final FabricViewStateManager mFabricViewStateManager = new FabricViewStateManager();
private @Nullable ReactScrollViewMaintainVisibleContentPositionData
mMaintainVisibleContentPositionData;
private @Nullable WeakReference<View> firstVisibleViewForMaintainVisibleContentPosition = null;

Copy link
Copy Markdown

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?

@evgeniy-skakun

Copy link
Copy Markdown

any updates ?

@SwikarBhattarai

Copy link
Copy Markdown

any update ?

@ponikar

Copy link
Copy Markdown

Any updates??

@roryabraham

roryabraham commented Sep 22, 2022

Copy link
Copy Markdown

@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 onStartReached in VirtualizedList and fixed some related bugs to support bidirectional pagination in FlatList/SectionList. All of this is still very much in development and has not been production-tested, but feel free to try it out if you want!

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 0.70.4, but really that's based on React Native's 0.70.1

fabOnReact added a commit to fabOnReact/react-native that referenced this pull request Sep 23, 2022
Use maintainVisibleContentPosition instead of existing functionality in FlatList implementation
PR 29466 react#29466
@darkbasic

Copy link
Copy Markdown

Any plan to upstream your efforts?

@roryabraham

roryabraham commented Sep 23, 2022

Copy link
Copy Markdown

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

@roryabraham

Copy link
Copy Markdown

@janicduplessis opened a new PR which implements maintainVisibleContentPosition on Android: #35049. We've been using it in production for some time now 🙂

@github-actions

Copy link
Copy Markdown

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.

@github-actionsgithub-actionsBot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Jun 14, 2023
@johanntony

Copy link
Copy Markdown

Here is an alternative native module implementation, for those who need maintainVisibleContentPosition on Android now but don't want to maintain your own RN fork.

packagecom.yourapp;
importandroid.view.View;
importcom.facebook.react.bridge.Promise;
importcom.facebook.react.bridge.ReactApplicationContext;
importcom.facebook.react.bridge.ReactContextBaseJavaModule;
importcom.facebook.react.bridge.ReactMethod;
importcom.facebook.react.uimanager.IllegalViewOperationException;
importcom.facebook.react.uimanager.NativeViewHierarchyManager;
importcom.facebook.react.uimanager.ReactShadowNode;
importcom.facebook.react.uimanager.UIBlock;
importcom.facebook.react.uimanager.UIImplementation;
importcom.facebook.react.uimanager.UIManagerModule;
importcom.facebook.react.uimanager.UIManagerModuleListener;
importcom.facebook.react.views.scroll.ReactScrollView;
importcom.facebook.react.views.view.ReactViewGroup;
importjava.util.HashMap;
publicclassScrollViewMagicModuleextendsReactContextBaseJavaModule {
privatefinalReactApplicationContextreactContext;
privateHashMap<Integer, UIManagerModuleListener> uiManagerModuleListeners;
publicScrollViewMagicModule(ReactApplicationContextreactContext) {
super(reactContext);
this.reactContext = reactContext;
}
@OverridepublicStringgetName() {
return"ScrollViewMagic";
}
@Overridepublicvoidinitialize() {
super.initialize();
this.uiManagerModuleListeners = newHashMap<>();
}
@ReactMethodpublicvoidenableMaintainVisibleContentPosition(finalintviewTag, finalPromisepromise) {
finalUIManagerModuleuiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
this.reactContext.runOnUiQueueThread(newRunnable() {
@Overridepublicvoidrun() {
try {
finalReactScrollViewscrollView = (ReactScrollView)uiManagerModule.resolveView(viewTag);
finalUIManagerModuleListeneruiManagerModuleListener = newUIManagerModuleListener() {
privateintminIndexForVisible = 0;
privateintprevFirstVisibleTop = 0;
privateViewfirstVisibleView = null;
@OverridepublicvoidwillDispatchViewUpdates(finalUIManagerModuleuiManagerModule) {
uiManagerModule.prependUIBlock(newUIBlock() {
@Overridepublicvoidexecute(NativeViewHierarchyManagernativeViewHierarchyManager) {
ReactViewGroupmContentView = (ReactViewGroup)scrollView.getChildAt(0);
if (mContentView == null) return;
for (intii = minIndexForVisible; ii < mContentView.getChildCount(); ++ii) {
Viewsubview = mContentView.getChildAt(ii);
if (subview.getTop() >= scrollView.getScrollY()) {
prevFirstVisibleTop = subview.getTop();
firstVisibleView = subview;
break;
}
}
}
});
UIImplementation.LayoutUpdateListenerlayoutUpdateListener = newUIImplementation.LayoutUpdateListener() {
@OverridepublicvoidonLayoutUpdated(ReactShadowNoderoot) {
if (firstVisibleView == null) return;
intdeltaY = firstVisibleView.getTop() - prevFirstVisibleTop;
if (Math.abs(deltaY) > 0) {
scrollView.setScrollY(scrollView.getScrollY() + deltaY);
}
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
}
};
uiManagerModule.getUIImplementation().setLayoutUpdateListener(layoutUpdateListener);
}
};
uiManagerModule.addUIManagerListener(uiManagerModuleListener);
intkey = uiManagerModuleListeners.size() + 1;
uiManagerModuleListeners.put(key, uiManagerModuleListener);
promise.resolve(key);
} catch(IllegalViewOperationExceptione) {
promise.resolve(-1);
}
}
});
}
@ReactMethodpublicvoiddisableMaintainVisibleContentPosition(intkey, Promisepromise) {
if (key >= 0) {
finalUIManagerModuleuiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
uiManagerModule.removeUIManagerListener(uiManagerModuleListeners.remove(key));
}
promise.resolve(null);
}
}

JS side:

constnativeModule: {enableMaintainVisibleContentPosition(viewTag: number): Promise<number>;disableMaintainVisibleContentPosition(handle: number): Promise<void>;}=NativeModules.ScrollViewMagic;
// `ref` is the ref to your ScrollView / FlatListuseEffect(()=>{letcleanupPromise: Promise<number>|undefined;if(Constants.isAndroid){constviewTag=findNodeHandle(ref.current);cleanupPromise=nativeModule.enableMaintainVisibleContentPosition(viewTag);}return()=>{voidcleanupPromise?.then((handle)=>{voidnativeModule.disableMaintainVisibleContentPosition(handle);});};},[ref]);

有没有iOS端的实现

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.Needs: React Native Team AttentionPlatform: AndroidAndroid applications.Shared with MetaApplied via automation to indicate that an Issue or Pull Request has been shared with the team.StaleThere has been a lack of activity on this issue and it may be closed soon.Type: EnhancementA new feature or enhancement of an existing feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

maintainVisibleContentPosition is not available on android

20 participants

@maxbth@analysis-bot@stackia@chrisglein@Strate@jjd314@tslater@vishalnarkhede@fabOnReact@aanation@ALexandreM75013@markoj3s@facebook-github-bot@yungsters@kylerjensen@stopachka@numandev1@bigmoveus@andresribeiro@evgeniy-skakun