Skip to content

Commit

Permalink
Add support for SectionList
Browse files Browse the repository at this point in the history
  • Loading branch information
eragon512 committed May 21, 2022
1 parent 4fd8789 commit d0a7a0e
Show file tree
Hide file tree
Showing 5 changed files with 291 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Example/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
147 changes: 147 additions & 0 deletions Example/src/SectionListExample.tsx
Original file line number Diff line number Diff line change
@@ -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}) => (
<TouchableOpacity onPress={onPress} style={styles.addMoreButton}>
<Text style={styles.addMoreButtonText}>Add 5 items from this side</Text>
</TouchableOpacity>
);

const ListItem = ({item}: {item: Item}) => (
<View style={styles.listItem}>
<Text>List item: {item.value}</Text>
</View>
);

const ListHeader = ({
section,
}: {
section: SectionListData<Item, {title: string}>;
}) => (
<View style={styles.listTitle}>
<Text>Title: {section.title}</Text>
</View>
);

// 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 (
<SafeAreaView style={styles.safeArea}>
<AddMoreButton onPress={addToStart} />
<View style={styles.listContainer}>
<SectionList
sections={sections}
keyExtractor={(item) => item.id}
maintainVisibleContentPosition={{
minIndexForVisible: 1,
}}
renderItem={ListItem}
renderSectionHeader={ListHeader}
/>
</View>
<AddMoreButton onPress={addToEnd} />
</SafeAreaView>
);
};

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',
},
});
137 changes: 137 additions & 0 deletions src/SectionList.android.tsx
Original file line number Diff line number Diff line change
@@ -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(
<ItemT extends any, SectionT = DefaultSectionT>(
props: RNSectionListProps<ItemT, SectionT>,
forwardedRef:
| ((instance: RNSectionList<ItemT, SectionT> | null) => void)
| MutableRefObject<RNSectionList<ItemT, SectionT> | null>
| null
) => {
const sectionListRef = useRef<RNSectionList<ItemT, SectionT> | null>(null);
const isMvcpEnabledNative = useRef<boolean>(false);
const handle = useRef<number | null>(null);
const enableMvcpRetriesCount = useRef<number>(0);
const isMvcpPropPresentRef = useRef(!!props.maintainVisibleContentPosition);

const autoscrollToTopThreshold = useRef<number | null>(
props.maintainVisibleContentPosition?.autoscrollToTopThreshold ||
-Number.MAX_SAFE_INTEGER
);
const minIndexForVisible = useRef<number>(
props.maintainVisibleContentPosition?.minIndexForVisible || 1
);
const retryTimeoutId = useRef<NodeJS.Timeout>();
const debounceTimeoutId = useRef<NodeJS.Timeout>();
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<ItemT, SectionT>) => 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 <RNSectionList<ItemT, SectionT> ref={refCallback} {...props} />;
}
);
3 changes: 3 additions & 0 deletions src/SectionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { SectionList } from 'react-native';

export default SectionList;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as FlatList } from './FlatList';
export { default as ScrollView } from './ScrollView';
export { default as SectionList } from './SectionList';

0 comments on commit d0a7a0e

Please sign in to comment.