Skip to content

Commit 383cc6c

Browse files
Merge pull request #15 from GetStream/vishal/improved-implementation
Improving implementation to fix NativeModule exception issue
2 parents 77d49a1 + 78430b5 commit 383cc6c

File tree

9 files changed

+281
-231
lines changed

9 files changed

+281
-231
lines changed

android/src/main/java/com/mvcpscrollviewmanager/MvcpScrollViewManagerModule.java

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,16 @@
1818

1919
import java.util.HashMap;
2020

21-
public class MvcpScrollViewManagerModule extends ReactContextBaseJavaModule {
21+
/**
22+
* Holds the required values for layoutUpdateListener.
23+
*/
24+
class ScrollViewUIHolders {
25+
static int prevFirstVisibleTop = 0;
26+
static View firstVisibleView = null;
27+
static int currentScrollY = 0;
28+
}
2229

30+
public class MvcpScrollViewManagerModule extends ReactContextBaseJavaModule {
2331
private final ReactApplicationContext reactContext;
2432
private HashMap<Integer, UIManagerModuleListener> uiManagerModuleListeners;
2533

@@ -48,60 +56,51 @@ public void run() {
4856
try {
4957
final ReactScrollView scrollView = (ReactScrollView)uiManagerModule.resolveView(viewTag);
5058
final UIManagerModuleListener uiManagerModuleListener = new UIManagerModuleListener() {
51-
private int prevFirstVisibleTop = 0;
52-
private View firstVisibleView = null;
53-
private int currentScrollY = 0;
5459
@Override
5560
public void willDispatchViewUpdates(final UIManagerModule uiManagerModule) {
56-
uiManagerModule.prependUIBlock(new UIBlock() {
57-
@Override
58-
public void execute(NativeViewHierarchyManager nativeViewHierarchyManager) {
59-
ReactViewGroup mContentView = (ReactViewGroup)scrollView.getChildAt(0);
60-
if (mContentView == null) return;
61-
62-
currentScrollY = scrollView.getScrollY();
63-
64-
for (int ii = minIndexForVisible; ii < mContentView.getChildCount(); ++ii) {
65-
View subview = mContentView.getChildAt(ii);
66-
if (subview.getTop() >= currentScrollY) {
67-
prevFirstVisibleTop = subview.getTop();
68-
firstVisibleView = subview;
69-
break;
70-
}
71-
}
61+
ReactViewGroup mContentView = (ReactViewGroup)scrollView.getChildAt(0);
62+
if (mContentView == null) return;
63+
64+
ScrollViewUIHolders.currentScrollY = scrollView.getScrollY();
65+
66+
for (int ii = minIndexForVisible; ii < mContentView.getChildCount(); ++ii) {
67+
View subview = mContentView.getChildAt(ii);
68+
if (subview.getTop() >= ScrollViewUIHolders.currentScrollY) {
69+
ScrollViewUIHolders.prevFirstVisibleTop = subview.getTop();
70+
ScrollViewUIHolders.firstVisibleView = subview;
71+
break;
7272
}
73-
});
73+
}
74+
}
75+
};
7476

75-
UIImplementation.LayoutUpdateListener layoutUpdateListener = new UIImplementation.LayoutUpdateListener() {
76-
@Override
77-
public void onLayoutUpdated(ReactShadowNode root) {
78-
if (firstVisibleView == null) return;
77+
UIImplementation.LayoutUpdateListener layoutUpdateListener = new UIImplementation.LayoutUpdateListener() {
78+
@Override
79+
public void onLayoutUpdated(ReactShadowNode root) {
80+
if (ScrollViewUIHolders.firstVisibleView == null) return;
7981

80-
int deltaY = firstVisibleView.getTop() - prevFirstVisibleTop;
82+
int deltaY = ScrollViewUIHolders.firstVisibleView.getTop() - ScrollViewUIHolders.prevFirstVisibleTop;
8183

8284

83-
if (Math.abs(deltaY) > 1) {
84-
boolean isWithinThreshold = currentScrollY <= autoscrollToTopThreshold;
85-
scrollView.setScrollY(currentScrollY + deltaY);
85+
if (Math.abs(deltaY) > 1) {
86+
boolean isWithinThreshold = ScrollViewUIHolders.currentScrollY <= autoscrollToTopThreshold;
87+
scrollView.setScrollY(ScrollViewUIHolders.currentScrollY + deltaY);
8688

87-
// If the offset WAS within the threshold of the start, animate to the start.
88-
if (isWithinThreshold) {
89-
scrollView.smoothScrollTo(scrollView.getScrollX(), 0);
90-
}
91-
}
92-
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
89+
// If the offset WAS within the threshold of the start, animate to the start.
90+
if (isWithinThreshold) {
91+
scrollView.smoothScrollTo(scrollView.getScrollX(), 0);
9392
}
94-
};
95-
96-
uiManagerModule.getUIImplementation().setLayoutUpdateListener(layoutUpdateListener);
93+
}
9794
}
9895
};
96+
97+
uiManagerModule.getUIImplementation().setLayoutUpdateListener(layoutUpdateListener);
9998
uiManagerModule.addUIManagerListener(uiManagerModuleListener);
10099
int key = uiManagerModuleListeners.size() + 1;
101100
uiManagerModuleListeners.put(key, uiManagerModuleListener);
102101
promise.resolve(key);
103102
} catch(IllegalViewOperationException e) {
104-
promise.resolve(-1);
103+
promise.reject(e);
105104
}
106105
}
107106
});
@@ -113,9 +112,10 @@ public void disableMaintainVisibleContentPosition(int key, Promise promise) {
113112
if (key >= 0) {
114113
final UIManagerModule uiManagerModule = this.reactContext.getNativeModule(UIManagerModule.class);
115114
uiManagerModule.removeUIManagerListener(uiManagerModuleListeners.remove(key));
115+
uiManagerModule.getUIImplementation().removeLayoutUpdateListener();
116116
}
117117
promise.resolve(null);
118-
} catch (IllegalViewOperationException e) {
118+
} catch (Exception e) {
119119
promise.resolve(-1);
120120
}
121121
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
22
"name": "@stream-io/flat-list-mvcp",
3-
"version": "0.0.9",
3+
"version": "0.10.0-beta.1",
44
"description": "`maintainVisibleContentPosition` support for Android react-native",
55
"main": "lib/commonjs/index",
66
"module": "lib/module/index",
77
"types": "lib/typescript/src/index.d.ts",
8-
"react-native": "lib/module/index",
8+
"react-native": "src/index",
99
"source": "src/index",
1010
"files": [
1111
"src",

src/FlatList.android.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, { MutableRefObject, useEffect, useRef } from 'react';
2+
import { FlatList, FlatListProps, NativeModules, Platform } from 'react-native';
3+
4+
export const ScrollViewManager = NativeModules.MvcpScrollViewManager;
5+
6+
export default (React.forwardRef(
7+
<T extends any>(
8+
props: FlatListProps<T>,
9+
forwardedRef:
10+
| ((instance: FlatList<T> | null) => void)
11+
| MutableRefObject<FlatList<T> | null>
12+
| null
13+
) => {
14+
const { maintainVisibleContentPosition: mvcp } = props;
15+
16+
const flRef = useRef<FlatList<T> | null>(null);
17+
const isMvcpEnabled = useRef<any>(null);
18+
const autoscrollToTopThreshold = useRef<number | null>();
19+
const minIndexForVisible = useRef<number>();
20+
const handle = useRef<any>(null);
21+
const enableMvcpRetries = useRef<number>(0);
22+
23+
const propAutoscrollToTopThreshold =
24+
mvcp?.autoscrollToTopThreshold || -Number.MAX_SAFE_INTEGER;
25+
const propMinIndexForVisible = mvcp?.minIndexForVisible || 1;
26+
27+
const hasMvcpChanged =
28+
autoscrollToTopThreshold.current !== propAutoscrollToTopThreshold ||
29+
minIndexForVisible.current !== propMinIndexForVisible;
30+
31+
const enableMvcp = () => {
32+
if (!flRef.current) return;
33+
34+
const scrollableNode = flRef.current.getScrollableNode();
35+
const enableMvcpPromise = ScrollViewManager.enableMaintainVisibleContentPosition(
36+
scrollableNode,
37+
autoscrollToTopThreshold.current,
38+
minIndexForVisible.current
39+
);
40+
41+
return enableMvcpPromise.then((_handle: number) => {
42+
handle.current = _handle;
43+
enableMvcpRetries.current = 0;
44+
});
45+
};
46+
47+
const enableMvcpWithRetries = () => {
48+
autoscrollToTopThreshold.current = propAutoscrollToTopThreshold;
49+
minIndexForVisible.current = propMinIndexForVisible;
50+
51+
return enableMvcp()?.catch(() => {
52+
/**
53+
* enableMaintainVisibleContentPosition from native module may throw IllegalViewOperationException,
54+
* in case view is not ready yet. In that case, lets do a retry!!
55+
*/
56+
if (enableMvcpRetries.current < 10) {
57+
setTimeout(enableMvcp, 10);
58+
enableMvcpRetries.current += 1;
59+
}
60+
});
61+
};
62+
63+
const disableMvcp: () => Promise<void> = () => {
64+
if (!ScrollViewManager || !handle?.current) {
65+
return Promise.resolve();
66+
}
67+
68+
return ScrollViewManager.disableMaintainVisibleContentPosition(
69+
handle.current
70+
);
71+
};
72+
73+
// We can only call enableMaintainVisibleContentPosition once the ref to underlying scrollview is ready.
74+
const resetMvcpIfNeeded = (): void => {
75+
if (!mvcp || Platform.OS !== 'android' || !flRef.current) {
76+
return;
77+
}
78+
79+
/**
80+
* If the enableMaintainVisibleContentPosition has already been called, then
81+
* lets not call it again, unless prop values of mvcp changed.
82+
*
83+
* This condition is important since `resetMvcpIfNeeded` gets called in refCallback,
84+
* which gets called by react on every update to list.
85+
*/
86+
if (isMvcpEnabled.current && !hasMvcpChanged) {
87+
return;
88+
}
89+
90+
isMvcpEnabled.current = true;
91+
disableMvcp().then(enableMvcpWithRetries);
92+
};
93+
94+
const refCallback: (instance: FlatList<T> | null) => void = (ref) => {
95+
flRef.current = ref;
96+
97+
resetMvcpIfNeeded();
98+
if (typeof forwardedRef === 'function') {
99+
forwardedRef(ref);
100+
} else if (forwardedRef) {
101+
forwardedRef.current = ref;
102+
}
103+
};
104+
105+
useEffect(() => {
106+
// disable before unmounting
107+
return () => {
108+
disableMvcp();
109+
};
110+
}, []);
111+
112+
return <FlatList<T> {...props} ref={refCallback} />;
113+
}
114+
) as unknown) as typeof FlatList;

src/FlatList.tsx

Lines changed: 2 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,3 @@
1-
import React, { MutableRefObject, useRef } from 'react';
2-
import { FlatList, FlatListProps, NativeModules, Platform } from 'react-native';
3-
import debounce from 'lodash/debounce';
1+
import { FlatList } from 'react-native';
42

5-
export const MvcpScrollViewManager = NativeModules.MvcpScrollViewManager;
6-
7-
const debouncedEnable = debounce(
8-
(
9-
enableMvcpPromise: MutableRefObject<Promise<any> | null>,
10-
disableMvcpPromise: MutableRefObject<Promise<any> | null>,
11-
viewTag: any,
12-
autoscrollToTopThreshold: number,
13-
minIndexForVisible: number
14-
) => {
15-
if (disableMvcpPromise.current) {
16-
disableMvcpPromise.current.then(() => {
17-
enableMvcpPromise.current = MvcpScrollViewManager.enableMaintainVisibleContentPosition(
18-
viewTag,
19-
autoscrollToTopThreshold,
20-
minIndexForVisible
21-
);
22-
});
23-
} else {
24-
enableMvcpPromise.current = MvcpScrollViewManager.enableMaintainVisibleContentPosition(
25-
viewTag,
26-
autoscrollToTopThreshold,
27-
minIndexForVisible
28-
);
29-
}
30-
},
31-
100,
32-
{
33-
trailing: true,
34-
}
35-
);
36-
37-
const debouncedDisable = debounce(
38-
(
39-
enableMvcpPromise: MutableRefObject<Promise<any> | null>,
40-
disableMvcpPromise: MutableRefObject<Promise<any> | null>
41-
) => {
42-
enableMvcpPromise.current?.then((handle) => {
43-
disableMvcpPromise.current = MvcpScrollViewManager.disableMaintainVisibleContentPosition(
44-
handle
45-
);
46-
});
47-
},
48-
50,
49-
{
50-
trailing: true,
51-
}
52-
);
53-
54-
export default (React.forwardRef(
55-
<T extends any>(
56-
props: FlatListProps<T>,
57-
forwardedRef:
58-
| ((instance: FlatList<T> | null) => void)
59-
| MutableRefObject<FlatList<T> | null>
60-
| null
61-
) => {
62-
const flRef = useRef<FlatList<T> | null>(null);
63-
const { maintainVisibleContentPosition: mvcp } = props;
64-
65-
const autoscrollToTopThreshold = useRef<number | null>();
66-
const minIndexForVisible = useRef<number>();
67-
const enableMvcpPromise = useRef<Promise<any> | null>(null);
68-
const disableMvcpPromise = useRef<Promise<any> | null>(null);
69-
70-
const resetMvcpIfNeeded = (): void => {
71-
if (!mvcp || Platform.OS !== 'android' || !flRef.current) {
72-
return;
73-
}
74-
75-
enableMvcpPromise &&
76-
enableMvcpPromise.current &&
77-
debouncedDisable(enableMvcpPromise, disableMvcpPromise);
78-
79-
autoscrollToTopThreshold.current = mvcp?.autoscrollToTopThreshold;
80-
minIndexForVisible.current = mvcp?.minIndexForVisible;
81-
82-
const viewTag = flRef.current.getScrollableNode();
83-
debouncedEnable(
84-
enableMvcpPromise,
85-
disableMvcpPromise,
86-
viewTag,
87-
autoscrollToTopThreshold.current || -Number.MAX_SAFE_INTEGER,
88-
minIndexForVisible.current || 1
89-
);
90-
};
91-
92-
return (
93-
<FlatList<T>
94-
{...props}
95-
ref={(ref) => {
96-
flRef.current = ref;
97-
98-
resetMvcpIfNeeded();
99-
if (typeof forwardedRef === 'function') {
100-
forwardedRef(ref);
101-
} else if (forwardedRef?.current) {
102-
forwardedRef.current = ref;
103-
}
104-
}}
105-
/>
106-
);
107-
}
108-
) as unknown) as typeof FlatList;
3+
export default FlatList;

0 commit comments

Comments
 (0)