diff --git a/Example/App.tsx b/Example/App.tsx
index fa6b637..2f33589 100644
--- a/Example/App.tsx
+++ b/Example/App.tsx
@@ -1,10 +1,12 @@
import React from 'react';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import ScrollViewExample from './src/ScrollViewExample';
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
import FlatListExample from './src/FlatListExample';
+import VirtualizedListExample from './src/VirtualizedListExample';
const App = () => {
- return ;
+ return ;
};
export default App;
diff --git a/Example/src/VirtualizedListExample.tsx b/Example/src/VirtualizedListExample.tsx
new file mode 100644
index 0000000..ccc9a52
--- /dev/null
+++ b/Example/src/VirtualizedListExample.tsx
@@ -0,0 +1,105 @@
+import React, {useState} from 'react';
+import {
+ View,
+ TouchableOpacity,
+ Text,
+ SafeAreaView,
+ StyleSheet,
+} from 'react-native';
+import {VirtualizedList} from '@stream-io/flat-list-mvcp';
+
+const AddMoreButton = ({onPress}) => (
+
+ Add 5 items from this side
+
+);
+
+const ListItem = ({item}) => (
+
+ List item: {item.value}
+
+);
+
+// Generate unique key list item.
+export const generateUniqueKey = () =>
+ `_${Math.random().toString(36).substr(2, 9)}`;
+
+export default () => {
+ const [numbers, setNumbers] = useState(
+ Array.from(Array(10).keys()).map((n) => ({
+ id: generateUniqueKey(),
+ value: n,
+ })),
+ );
+
+ const addToEnd = () => {
+ setNumbers((prev) => {
+ const additionalNumbers = Array.from(Array(5).keys()).map((n) => ({
+ id: generateUniqueKey(),
+ value: n + prev[prev.length - 1].value + 1,
+ }));
+
+ return prev.concat(additionalNumbers);
+ });
+ };
+
+ const addToStart = () => {
+ setNumbers((prev) => {
+ const additionalNumbers = Array.from(Array(5).keys())
+ .map((n) => ({
+ id: generateUniqueKey(),
+ value: prev[0].value - n - 1,
+ }))
+ .reverse();
+
+ return additionalNumbers.concat(prev);
+ });
+ };
+
+ return (
+
+
+
+ item.id}
+ maintainVisibleContentPosition={{
+ minIndexForVisible: 0,
+ }}
+ getItem={(data, index) => data[index]}
+ getItemCount={() => numbers.length}
+ renderItem={ListItem}
+ />
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ },
+ addMoreButton: {
+ padding: 8,
+ backgroundColor: '#008CBA',
+ alignItems: 'center',
+ },
+ addMoreButtonText: {
+ color: 'white',
+ },
+ listContainer: {
+ paddingVertical: 4,
+ flexGrow: 1,
+ flexShrink: 1,
+ backgroundColor: 'black',
+ },
+ listItem: {
+ flex: 1,
+ padding: 32,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderWidth: 8,
+ backgroundColor: 'white',
+ },
+});
diff --git a/src/VirtualizedList.android.tsx b/src/VirtualizedList.android.tsx
new file mode 100644
index 0000000..4d4b16f
--- /dev/null
+++ b/src/VirtualizedList.android.tsx
@@ -0,0 +1,122 @@
+import React, { MutableRefObject, useEffect, useRef } from 'react';
+import {
+ VirtualizedList,
+ VirtualizedListProps,
+ NativeModules,
+ Platform,
+} from 'react-native';
+
+export const ScrollViewManager = NativeModules.MvcpScrollViewManager;
+
+interface EnhancedVirtualizedList extends VirtualizedList {
+ getScrollableNode(): any;
+}
+
+export default (React.forwardRef(
+ (
+ props: VirtualizedListProps,
+ forwardedRef:
+ | ((instance: EnhancedVirtualizedList | null) => void)
+ | MutableRefObject | null>
+ | null
+ ) => {
+ const { maintainVisibleContentPosition: mvcp } = props;
+
+ const flRef = useRef | null>(null);
+ const isMvcpEnabled = useRef(null);
+ const autoscrollToTopThreshold = useRef();
+ const minIndexForVisible = useRef();
+ const handle = useRef(null);
+ const enableMvcpRetries = useRef(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 = () => {
+ 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(enableMvcpWithRetries, 10);
+ enableMvcpRetries.current += 1;
+ }
+ });
+ };
+
+ const disableMvcp: () => Promise = () => {
+ 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;
+ }
+ autoscrollToTopThreshold.current = propAutoscrollToTopThreshold;
+ minIndexForVisible.current = propMinIndexForVisible;
+
+ isMvcpEnabled.current = true;
+ disableMvcp().then(enableMvcpWithRetries);
+ };
+
+ const refCallback: (instance: EnhancedVirtualizedList | 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 {...props} ref={refCallback} />;
+ }
+) as unknown) as typeof VirtualizedList;
diff --git a/src/VirtualizedList.tsx b/src/VirtualizedList.tsx
new file mode 100644
index 0000000..111a230
--- /dev/null
+++ b/src/VirtualizedList.tsx
@@ -0,0 +1,3 @@
+import { VirtualizedList } from 'react-native';
+
+export default VirtualizedList;
diff --git a/src/index.ts b/src/index.ts
index 8729b8b..d154298 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,3 @@
export { default as FlatList } from './FlatList';
export { default as ScrollView } from './ScrollView';
+export { default as VirtualizedList } from './VirtualizedList';