From fcf74a2eed2503fd4541c37178ea75549e771154 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Mon, 1 Nov 2021 22:57:25 -0400 Subject: [PATCH] Add support for VirtualizedList --- Example/App.tsx | 4 +- Example/src/VirtualizedListExample.tsx | 105 +++++++++++++++++++++ src/VirtualizedList.android.tsx | 122 +++++++++++++++++++++++++ src/VirtualizedList.tsx | 3 + src/index.ts | 1 + 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 Example/src/VirtualizedListExample.tsx create mode 100644 src/VirtualizedList.android.tsx create mode 100644 src/VirtualizedList.tsx 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';