Skip to content

Commit

Permalink
Merge pull request #15 from GetStream/vishal/improved-implementation
Browse files Browse the repository at this point in the history
Improving implementation to fix NativeModule exception issue
  • Loading branch information
vishalnarkhede authored Apr 4, 2021
2 parents 77d49a1 + 78430b5 commit 383cc6c
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 231 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@

import java.util.HashMap;

public class MvcpScrollViewManagerModule extends ReactContextBaseJavaModule {
/**
* Holds the required values for layoutUpdateListener.
*/
class ScrollViewUIHolders {
static int prevFirstVisibleTop = 0;
static View firstVisibleView = null;
static int currentScrollY = 0;
}

public class MvcpScrollViewManagerModule extends ReactContextBaseJavaModule {
private final ReactApplicationContext reactContext;
private HashMap<Integer, UIManagerModuleListener> uiManagerModuleListeners;

Expand Down Expand Up @@ -48,60 +56,51 @@ public void run() {
try {
final ReactScrollView scrollView = (ReactScrollView)uiManagerModule.resolveView(viewTag);
final UIManagerModuleListener uiManagerModuleListener = new UIManagerModuleListener() {
private int prevFirstVisibleTop = 0;
private View firstVisibleView = null;
private int currentScrollY = 0;
@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;

currentScrollY = scrollView.getScrollY();

for (int ii = minIndexForVisible; ii < mContentView.getChildCount(); ++ii) {
View subview = mContentView.getChildAt(ii);
if (subview.getTop() >= currentScrollY) {
prevFirstVisibleTop = subview.getTop();
firstVisibleView = subview;
break;
}
}
ReactViewGroup mContentView = (ReactViewGroup)scrollView.getChildAt(0);
if (mContentView == null) return;

ScrollViewUIHolders.currentScrollY = scrollView.getScrollY();

for (int ii = minIndexForVisible; ii < mContentView.getChildCount(); ++ii) {
View subview = mContentView.getChildAt(ii);
if (subview.getTop() >= ScrollViewUIHolders.currentScrollY) {
ScrollViewUIHolders.prevFirstVisibleTop = subview.getTop();
ScrollViewUIHolders.firstVisibleView = subview;
break;
}
});
}
}
};

UIImplementation.LayoutUpdateListener layoutUpdateListener = new UIImplementation.LayoutUpdateListener() {
@Override
public void onLayoutUpdated(ReactShadowNode root) {
if (firstVisibleView == null) return;
UIImplementation.LayoutUpdateListener layoutUpdateListener = new UIImplementation.LayoutUpdateListener() {
@Override
public void onLayoutUpdated(ReactShadowNode root) {
if (ScrollViewUIHolders.firstVisibleView == null) return;

int deltaY = firstVisibleView.getTop() - prevFirstVisibleTop;
int deltaY = ScrollViewUIHolders.firstVisibleView.getTop() - ScrollViewUIHolders.prevFirstVisibleTop;


if (Math.abs(deltaY) > 1) {
boolean isWithinThreshold = currentScrollY <= autoscrollToTopThreshold;
scrollView.setScrollY(currentScrollY + deltaY);
if (Math.abs(deltaY) > 1) {
boolean isWithinThreshold = ScrollViewUIHolders.currentScrollY <= autoscrollToTopThreshold;
scrollView.setScrollY(ScrollViewUIHolders.currentScrollY + deltaY);

// If the offset WAS within the threshold of the start, animate to the start.
if (isWithinThreshold) {
scrollView.smoothScrollTo(scrollView.getScrollX(), 0);
}
}
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
// If the offset WAS within the threshold of the start, animate to the start.
if (isWithinThreshold) {
scrollView.smoothScrollTo(scrollView.getScrollX(), 0);
}
};

uiManagerModule.getUIImplementation().setLayoutUpdateListener(layoutUpdateListener);
}
}
};

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);
promise.reject(e);
}
}
});
Expand All @@ -113,9 +112,10 @@ public void disableMaintainVisibleContentPosition(int key, Promise promise) {
if (key >= 0) {
final UIManagerModule uiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
uiManagerModule.removeUIManagerListener(uiManagerModuleListeners.remove(key));
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
}
promise.resolve(null);
} catch (IllegalViewOperationException e) {
} catch (Exception e) {
promise.resolve(-1);
}
}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "@stream-io/flat-list-mvcp",
"version": "0.0.9",
"version": "0.10.0-beta.1",
"description": "`maintainVisibleContentPosition` support for Android react-native",
"main": "lib/commonjs/index",
"module": "lib/module/index",
"types": "lib/typescript/src/index.d.ts",
"react-native": "lib/module/index",
"react-native": "src/index",
"source": "src/index",
"files": [
"src",
Expand Down
114 changes: 114 additions & 0 deletions src/FlatList.android.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React, { MutableRefObject, useEffect, useRef } from 'react';
import { FlatList, FlatListProps, NativeModules, Platform } from 'react-native';

export const ScrollViewManager = NativeModules.MvcpScrollViewManager;

export default (React.forwardRef(
<T extends any>(
props: FlatListProps<T>,
forwardedRef:
| ((instance: FlatList<T> | null) => void)
| MutableRefObject<FlatList<T> | null>
| null
) => {
const { maintainVisibleContentPosition: mvcp } = props;

const flRef = useRef<FlatList<T> | null>(null);
const isMvcpEnabled = useRef<any>(null);
const autoscrollToTopThreshold = useRef<number | null>();
const minIndexForVisible = useRef<number>();
const handle = useRef<any>(null);
const enableMvcpRetries = useRef<number>(0);

const propAutoscrollToTopThreshold =
mvcp?.autoscrollToTopThreshold || -Number.MAX_SAFE_INTEGER;
const propMinIndexForVisible = mvcp?.minIndexForVisible || 1;

const hasMvcpChanged =
autoscrollToTopThreshold.current !== propAutoscrollToTopThreshold ||
minIndexForVisible.current !== propMinIndexForVisible;

const enableMvcp = () => {
if (!flRef.current) return;

const scrollableNode = flRef.current.getScrollableNode();
const enableMvcpPromise = ScrollViewManager.enableMaintainVisibleContentPosition(
scrollableNode,
autoscrollToTopThreshold.current,
minIndexForVisible.current
);

return enableMvcpPromise.then((_handle: number) => {
handle.current = _handle;
enableMvcpRetries.current = 0;
});
};

const enableMvcpWithRetries = () => {
autoscrollToTopThreshold.current = propAutoscrollToTopThreshold;
minIndexForVisible.current = propMinIndexForVisible;

return enableMvcp()?.catch(() => {
/**
* enableMaintainVisibleContentPosition from native module may throw IllegalViewOperationException,
* in case view is not ready yet. In that case, lets do a retry!!
*/
if (enableMvcpRetries.current < 10) {
setTimeout(enableMvcp, 10);
enableMvcpRetries.current += 1;
}
});
};

const disableMvcp: () => Promise<void> = () => {
if (!ScrollViewManager || !handle?.current) {
return Promise.resolve();
}

return ScrollViewManager.disableMaintainVisibleContentPosition(
handle.current
);
};

// We can only call enableMaintainVisibleContentPosition once the ref to underlying scrollview is ready.
const resetMvcpIfNeeded = (): void => {
if (!mvcp || Platform.OS !== 'android' || !flRef.current) {
return;
}

/**
* If the enableMaintainVisibleContentPosition has already been called, then
* lets not call it again, unless prop values of mvcp changed.
*
* This condition is important since `resetMvcpIfNeeded` gets called in refCallback,
* which gets called by react on every update to list.
*/
if (isMvcpEnabled.current && !hasMvcpChanged) {
return;
}

isMvcpEnabled.current = true;
disableMvcp().then(enableMvcpWithRetries);
};

const refCallback: (instance: FlatList<T> | null) => void = (ref) => {
flRef.current = ref;

resetMvcpIfNeeded();
if (typeof forwardedRef === 'function') {
forwardedRef(ref);
} else if (forwardedRef) {
forwardedRef.current = ref;
}
};

useEffect(() => {
// disable before unmounting
return () => {
disableMvcp();
};
}, []);

return <FlatList<T> {...props} ref={refCallback} />;
}
) as unknown) as typeof FlatList;
109 changes: 2 additions & 107 deletions src/FlatList.tsx
Original file line number Diff line number Diff line change
@@ -1,108 +1,3 @@
import React, { MutableRefObject, useRef } from 'react';
import { FlatList, FlatListProps, NativeModules, Platform } from 'react-native';
import debounce from 'lodash/debounce';
import { FlatList } from 'react-native';

export const MvcpScrollViewManager = NativeModules.MvcpScrollViewManager;

const debouncedEnable = debounce(
(
enableMvcpPromise: MutableRefObject<Promise<any> | null>,
disableMvcpPromise: MutableRefObject<Promise<any> | null>,
viewTag: any,
autoscrollToTopThreshold: number,
minIndexForVisible: number
) => {
if (disableMvcpPromise.current) {
disableMvcpPromise.current.then(() => {
enableMvcpPromise.current = MvcpScrollViewManager.enableMaintainVisibleContentPosition(
viewTag,
autoscrollToTopThreshold,
minIndexForVisible
);
});
} else {
enableMvcpPromise.current = MvcpScrollViewManager.enableMaintainVisibleContentPosition(
viewTag,
autoscrollToTopThreshold,
minIndexForVisible
);
}
},
100,
{
trailing: true,
}
);

const debouncedDisable = debounce(
(
enableMvcpPromise: MutableRefObject<Promise<any> | null>,
disableMvcpPromise: MutableRefObject<Promise<any> | null>
) => {
enableMvcpPromise.current?.then((handle) => {
disableMvcpPromise.current = MvcpScrollViewManager.disableMaintainVisibleContentPosition(
handle
);
});
},
50,
{
trailing: true,
}
);

export default (React.forwardRef(
<T extends any>(
props: FlatListProps<T>,
forwardedRef:
| ((instance: FlatList<T> | null) => void)
| MutableRefObject<FlatList<T> | null>
| null
) => {
const flRef = useRef<FlatList<T> | null>(null);
const { maintainVisibleContentPosition: mvcp } = props;

const autoscrollToTopThreshold = useRef<number | null>();
const minIndexForVisible = useRef<number>();
const enableMvcpPromise = useRef<Promise<any> | null>(null);
const disableMvcpPromise = useRef<Promise<any> | null>(null);

const resetMvcpIfNeeded = (): void => {
if (!mvcp || Platform.OS !== 'android' || !flRef.current) {
return;
}

enableMvcpPromise &&
enableMvcpPromise.current &&
debouncedDisable(enableMvcpPromise, disableMvcpPromise);

autoscrollToTopThreshold.current = mvcp?.autoscrollToTopThreshold;
minIndexForVisible.current = mvcp?.minIndexForVisible;

const viewTag = flRef.current.getScrollableNode();
debouncedEnable(
enableMvcpPromise,
disableMvcpPromise,
viewTag,
autoscrollToTopThreshold.current || -Number.MAX_SAFE_INTEGER,
minIndexForVisible.current || 1
);
};

return (
<FlatList<T>
{...props}
ref={(ref) => {
flRef.current = ref;

resetMvcpIfNeeded();
if (typeof forwardedRef === 'function') {
forwardedRef(ref);
} else if (forwardedRef?.current) {
forwardedRef.current = ref;
}
}}
/>
);
}
) as unknown) as typeof FlatList;
export default FlatList;
Loading

0 comments on commit 383cc6c

Please sign in to comment.