From d0a7a0efabb55e6ed6da32f710291f68717e7bc0 Mon Sep 17 00:00:00 2001 From: eragon512 Date: Sat, 21 May 2022 15:31:56 +0530 Subject: [PATCH] Add support for SectionList --- Example/App.tsx | 3 + Example/src/SectionListExample.tsx | 147 +++++++++++++++++++++++++++++ src/SectionList.android.tsx | 137 +++++++++++++++++++++++++++ src/SectionList.tsx | 3 + src/index.ts | 1 + 5 files changed, 291 insertions(+) create mode 100644 Example/src/SectionListExample.tsx create mode 100644 src/SectionList.android.tsx create mode 100644 src/SectionList.tsx diff --git a/Example/App.tsx b/Example/App.tsx index e7dda40..51d6546 100644 --- a/Example/App.tsx +++ b/Example/App.tsx @@ -2,6 +2,9 @@ import React from 'react'; // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-unused-vars import ScrollViewExample from './src/ScrollViewExample'; +// @ts-ignore +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import SectionListExample from './src/SectionListExample'; import FlatListExample from './src/FlatListExample'; const App = () => { diff --git a/Example/src/SectionListExample.tsx b/Example/src/SectionListExample.tsx new file mode 100644 index 0000000..13b47e8 --- /dev/null +++ b/Example/src/SectionListExample.tsx @@ -0,0 +1,147 @@ +import React, {useState} from 'react'; +import { + View, + TouchableOpacity, + Text, + SafeAreaView, + StyleSheet, + SectionListData, +} from 'react-native'; +import {SectionList} from '@stream-io/flat-list-mvcp'; + +type Item = { + id: string; + value: number; +}; + +const AddMoreButton = ({onPress}: {onPress: () => void}) => ( + + Add 5 items from this side + +); + +const ListItem = ({item}: {item: Item}) => ( + + List item: {item.value} + +); + +const ListHeader = ({ + section, +}: { + section: SectionListData; +}) => ( + + Title: {section.title} + +); + +// Generate unique key list item. +export const generateUniqueKey = () => + `_${Math.random().toString(36).substr(2, 9)}`; + +const SectionListExample = () => { + const [sections, setSections] = useState([ + { + title: 'Section 0 to 4', + data: Array.from(Array(5).keys()).map((n) => ({ + id: generateUniqueKey(), + value: n, + })), + }, + { + title: 'Section 5 to 9', + data: Array.from(Array(5).keys()).map((n) => ({ + id: generateUniqueKey(), + value: n + 5, + })), + }, + ]); + + const addToEnd = () => { + setSections((prev) => { + const additionalSection = { + title: `Section ${prev.length * 5} to ${prev.length * 5 + 4}`, + data: Array.from(Array(5).keys()).map((n) => ({ + id: generateUniqueKey(), + value: n + prev.length * 5, + })), + }; + + return prev.concat(additionalSection); + }); + }; + + const addToStart = () => { + setSections((prev) => { + const additionalSection = { + title: `Section ${prev[0].data[0].value - 5} to ${ + prev[0].data[0].value - 1 + }`, + data: Array.from(Array(5).keys()) + .map((n) => ({ + id: generateUniqueKey(), + value: prev[0].data[0].value - n - 1, + })) + .reverse(), + }; + + return [additionalSection].concat(prev); + }); + }; + + return ( + + + + item.id} + maintainVisibleContentPosition={{ + minIndexForVisible: 1, + }} + renderItem={ListItem} + renderSectionHeader={ListHeader} + /> + + + + ); +}; + +export default SectionListExample; + +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', + }, + listTitle: { + flex: 1, + padding: 16, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'grey', + }, +}); diff --git a/src/SectionList.android.tsx b/src/SectionList.android.tsx new file mode 100644 index 0000000..c269f6e --- /dev/null +++ b/src/SectionList.android.tsx @@ -0,0 +1,137 @@ +import React, { MutableRefObject, useEffect, useRef } from 'react'; +import { + DefaultSectionT, + NativeModules, + Platform, + SectionList as RNSectionList, + SectionListProps as RNSectionListProps, +} from 'react-native'; + +export const ScrollViewManager = NativeModules.MvcpScrollViewManager; + +export default React.forwardRef( + ( + props: RNSectionListProps, + forwardedRef: + | ((instance: RNSectionList | null) => void) + | MutableRefObject | null> + | null + ) => { + const sectionListRef = useRef | null>(null); + const isMvcpEnabledNative = useRef(false); + const handle = useRef(null); + const enableMvcpRetriesCount = useRef(0); + const isMvcpPropPresentRef = useRef(!!props.maintainVisibleContentPosition); + + const autoscrollToTopThreshold = useRef( + props.maintainVisibleContentPosition?.autoscrollToTopThreshold || + -Number.MAX_SAFE_INTEGER + ); + const minIndexForVisible = useRef( + props.maintainVisibleContentPosition?.minIndexForVisible || 1 + ); + const retryTimeoutId = useRef(); + const debounceTimeoutId = useRef(); + const disableMvcpRef = useRef(async () => { + isMvcpEnabledNative.current = false; + if (!handle?.current) { + return; + } + await ScrollViewManager.disableMaintainVisibleContentPosition( + handle.current + ); + }); + const enableMvcpWithRetriesRef = useRef(() => { + // debounce to wait till consecutive mvcp enabling + // this ensures that always previous handles are disabled first + if (debounceTimeoutId.current) { + clearTimeout(debounceTimeoutId.current); + } + debounceTimeoutId.current = setTimeout(async () => { + // disable any previous enabled handles + await disableMvcpRef.current(); + + if ( + !sectionListRef.current || + !isMvcpPropPresentRef.current || + isMvcpEnabledNative.current || + Platform.OS !== 'android' + ) { + return; + } + const scrollableNode = sectionListRef.current.getScrollableNode(); + + try { + handle.current = await ScrollViewManager.enableMaintainVisibleContentPosition( + scrollableNode, + autoscrollToTopThreshold.current, + minIndexForVisible.current + ); + } catch (error: any) { + /** + * enableMaintainVisibleContentPosition from native module may throw IllegalViewOperationException, + * in case view is not ready yet. In that case, lets do a retry!! (max of 10 tries) + */ + if (enableMvcpRetriesCount.current < 10) { + retryTimeoutId.current = setTimeout( + enableMvcpWithRetriesRef.current, + 100 + ); + enableMvcpRetriesCount.current += 1; + } + } + }, 300); + }); + + useEffect(() => { + // when the mvcp prop changes + // enable natively again, if the prop has changed + const propAutoscrollToTopThreshold = + props.maintainVisibleContentPosition?.autoscrollToTopThreshold || + -Number.MAX_SAFE_INTEGER; + const propMinIndexForVisible = + props.maintainVisibleContentPosition?.minIndexForVisible || 1; + const hasMvcpChanged = + autoscrollToTopThreshold.current !== propAutoscrollToTopThreshold || + minIndexForVisible.current !== propMinIndexForVisible || + isMvcpPropPresentRef.current !== !!props.maintainVisibleContentPosition; + + if (hasMvcpChanged) { + enableMvcpRetriesCount.current = 0; + autoscrollToTopThreshold.current = propAutoscrollToTopThreshold; + minIndexForVisible.current = propMinIndexForVisible; + isMvcpPropPresentRef.current = !!props.maintainVisibleContentPosition; + enableMvcpWithRetriesRef.current(); + } + }, [props.maintainVisibleContentPosition]); + + const refCallback = useRef< + (instance: RNSectionList) => void + >((ref) => { + sectionListRef.current = ref; + enableMvcpWithRetriesRef.current(); + if (typeof forwardedRef === 'function') { + forwardedRef(ref); + } else if (forwardedRef) { + forwardedRef.current = ref; + } + }).current; + + useEffect(() => { + const disableMvcp = disableMvcpRef.current; + return () => { + // clean up the retry mechanism + if (debounceTimeoutId.current) { + clearTimeout(debounceTimeoutId.current); + } + // clean up any debounce + if (debounceTimeoutId.current) { + clearTimeout(debounceTimeoutId.current); + } + disableMvcp(); + }; + }, []); + + return ref={refCallback} {...props} />; + } +); diff --git a/src/SectionList.tsx b/src/SectionList.tsx new file mode 100644 index 0000000..116348d --- /dev/null +++ b/src/SectionList.tsx @@ -0,0 +1,3 @@ +import { SectionList } from 'react-native'; + +export default SectionList; diff --git a/src/index.ts b/src/index.ts index 8729b8b..5d52bc6 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 SectionList } from './SectionList';