Skip to content

Commit d0a7a0e

Browse files
committed
Add support for SectionList
1 parent 4fd8789 commit d0a7a0e

File tree

5 files changed

+291
-0
lines changed

5 files changed

+291
-0
lines changed

Example/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import React from 'react';
22
// @ts-ignore
33
// eslint-disable-next-line @typescript-eslint/no-unused-vars
44
import ScrollViewExample from './src/ScrollViewExample';
5+
// @ts-ignore
6+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7+
import SectionListExample from './src/SectionListExample';
58
import FlatListExample from './src/FlatListExample';
69

710
const App = () => {

Example/src/SectionListExample.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, {useState} from 'react';
2+
import {
3+
View,
4+
TouchableOpacity,
5+
Text,
6+
SafeAreaView,
7+
StyleSheet,
8+
SectionListData,
9+
} from 'react-native';
10+
import {SectionList} from '@stream-io/flat-list-mvcp';
11+
12+
type Item = {
13+
id: string;
14+
value: number;
15+
};
16+
17+
const AddMoreButton = ({onPress}: {onPress: () => void}) => (
18+
<TouchableOpacity onPress={onPress} style={styles.addMoreButton}>
19+
<Text style={styles.addMoreButtonText}>Add 5 items from this side</Text>
20+
</TouchableOpacity>
21+
);
22+
23+
const ListItem = ({item}: {item: Item}) => (
24+
<View style={styles.listItem}>
25+
<Text>List item: {item.value}</Text>
26+
</View>
27+
);
28+
29+
const ListHeader = ({
30+
section,
31+
}: {
32+
section: SectionListData<Item, {title: string}>;
33+
}) => (
34+
<View style={styles.listTitle}>
35+
<Text>Title: {section.title}</Text>
36+
</View>
37+
);
38+
39+
// Generate unique key list item.
40+
export const generateUniqueKey = () =>
41+
`_${Math.random().toString(36).substr(2, 9)}`;
42+
43+
const SectionListExample = () => {
44+
const [sections, setSections] = useState([
45+
{
46+
title: 'Section 0 to 4',
47+
data: Array.from(Array(5).keys()).map((n) => ({
48+
id: generateUniqueKey(),
49+
value: n,
50+
})),
51+
},
52+
{
53+
title: 'Section 5 to 9',
54+
data: Array.from(Array(5).keys()).map((n) => ({
55+
id: generateUniqueKey(),
56+
value: n + 5,
57+
})),
58+
},
59+
]);
60+
61+
const addToEnd = () => {
62+
setSections((prev) => {
63+
const additionalSection = {
64+
title: `Section ${prev.length * 5} to ${prev.length * 5 + 4}`,
65+
data: Array.from(Array(5).keys()).map((n) => ({
66+
id: generateUniqueKey(),
67+
value: n + prev.length * 5,
68+
})),
69+
};
70+
71+
return prev.concat(additionalSection);
72+
});
73+
};
74+
75+
const addToStart = () => {
76+
setSections((prev) => {
77+
const additionalSection = {
78+
title: `Section ${prev[0].data[0].value - 5} to ${
79+
prev[0].data[0].value - 1
80+
}`,
81+
data: Array.from(Array(5).keys())
82+
.map((n) => ({
83+
id: generateUniqueKey(),
84+
value: prev[0].data[0].value - n - 1,
85+
}))
86+
.reverse(),
87+
};
88+
89+
return [additionalSection].concat(prev);
90+
});
91+
};
92+
93+
return (
94+
<SafeAreaView style={styles.safeArea}>
95+
<AddMoreButton onPress={addToStart} />
96+
<View style={styles.listContainer}>
97+
<SectionList
98+
sections={sections}
99+
keyExtractor={(item) => item.id}
100+
maintainVisibleContentPosition={{
101+
minIndexForVisible: 1,
102+
}}
103+
renderItem={ListItem}
104+
renderSectionHeader={ListHeader}
105+
/>
106+
</View>
107+
<AddMoreButton onPress={addToEnd} />
108+
</SafeAreaView>
109+
);
110+
};
111+
112+
export default SectionListExample;
113+
114+
const styles = StyleSheet.create({
115+
safeArea: {
116+
flex: 1,
117+
},
118+
addMoreButton: {
119+
padding: 8,
120+
backgroundColor: '#008CBA',
121+
alignItems: 'center',
122+
},
123+
addMoreButtonText: {
124+
color: 'white',
125+
},
126+
listContainer: {
127+
paddingVertical: 4,
128+
flexGrow: 1,
129+
flexShrink: 1,
130+
backgroundColor: 'black',
131+
},
132+
listItem: {
133+
flex: 1,
134+
padding: 32,
135+
justifyContent: 'center',
136+
alignItems: 'center',
137+
borderWidth: 8,
138+
backgroundColor: 'white',
139+
},
140+
listTitle: {
141+
flex: 1,
142+
padding: 16,
143+
justifyContent: 'center',
144+
alignItems: 'center',
145+
backgroundColor: 'grey',
146+
},
147+
});

src/SectionList.android.tsx

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React, { MutableRefObject, useEffect, useRef } from 'react';
2+
import {
3+
DefaultSectionT,
4+
NativeModules,
5+
Platform,
6+
SectionList as RNSectionList,
7+
SectionListProps as RNSectionListProps,
8+
} from 'react-native';
9+
10+
export const ScrollViewManager = NativeModules.MvcpScrollViewManager;
11+
12+
export default React.forwardRef(
13+
<ItemT extends any, SectionT = DefaultSectionT>(
14+
props: RNSectionListProps<ItemT, SectionT>,
15+
forwardedRef:
16+
| ((instance: RNSectionList<ItemT, SectionT> | null) => void)
17+
| MutableRefObject<RNSectionList<ItemT, SectionT> | null>
18+
| null
19+
) => {
20+
const sectionListRef = useRef<RNSectionList<ItemT, SectionT> | null>(null);
21+
const isMvcpEnabledNative = useRef<boolean>(false);
22+
const handle = useRef<number | null>(null);
23+
const enableMvcpRetriesCount = useRef<number>(0);
24+
const isMvcpPropPresentRef = useRef(!!props.maintainVisibleContentPosition);
25+
26+
const autoscrollToTopThreshold = useRef<number | null>(
27+
props.maintainVisibleContentPosition?.autoscrollToTopThreshold ||
28+
-Number.MAX_SAFE_INTEGER
29+
);
30+
const minIndexForVisible = useRef<number>(
31+
props.maintainVisibleContentPosition?.minIndexForVisible || 1
32+
);
33+
const retryTimeoutId = useRef<NodeJS.Timeout>();
34+
const debounceTimeoutId = useRef<NodeJS.Timeout>();
35+
const disableMvcpRef = useRef(async () => {
36+
isMvcpEnabledNative.current = false;
37+
if (!handle?.current) {
38+
return;
39+
}
40+
await ScrollViewManager.disableMaintainVisibleContentPosition(
41+
handle.current
42+
);
43+
});
44+
const enableMvcpWithRetriesRef = useRef(() => {
45+
// debounce to wait till consecutive mvcp enabling
46+
// this ensures that always previous handles are disabled first
47+
if (debounceTimeoutId.current) {
48+
clearTimeout(debounceTimeoutId.current);
49+
}
50+
debounceTimeoutId.current = setTimeout(async () => {
51+
// disable any previous enabled handles
52+
await disableMvcpRef.current();
53+
54+
if (
55+
!sectionListRef.current ||
56+
!isMvcpPropPresentRef.current ||
57+
isMvcpEnabledNative.current ||
58+
Platform.OS !== 'android'
59+
) {
60+
return;
61+
}
62+
const scrollableNode = sectionListRef.current.getScrollableNode();
63+
64+
try {
65+
handle.current = await ScrollViewManager.enableMaintainVisibleContentPosition(
66+
scrollableNode,
67+
autoscrollToTopThreshold.current,
68+
minIndexForVisible.current
69+
);
70+
} catch (error: any) {
71+
/**
72+
* enableMaintainVisibleContentPosition from native module may throw IllegalViewOperationException,
73+
* in case view is not ready yet. In that case, lets do a retry!! (max of 10 tries)
74+
*/
75+
if (enableMvcpRetriesCount.current < 10) {
76+
retryTimeoutId.current = setTimeout(
77+
enableMvcpWithRetriesRef.current,
78+
100
79+
);
80+
enableMvcpRetriesCount.current += 1;
81+
}
82+
}
83+
}, 300);
84+
});
85+
86+
useEffect(() => {
87+
// when the mvcp prop changes
88+
// enable natively again, if the prop has changed
89+
const propAutoscrollToTopThreshold =
90+
props.maintainVisibleContentPosition?.autoscrollToTopThreshold ||
91+
-Number.MAX_SAFE_INTEGER;
92+
const propMinIndexForVisible =
93+
props.maintainVisibleContentPosition?.minIndexForVisible || 1;
94+
const hasMvcpChanged =
95+
autoscrollToTopThreshold.current !== propAutoscrollToTopThreshold ||
96+
minIndexForVisible.current !== propMinIndexForVisible ||
97+
isMvcpPropPresentRef.current !== !!props.maintainVisibleContentPosition;
98+
99+
if (hasMvcpChanged) {
100+
enableMvcpRetriesCount.current = 0;
101+
autoscrollToTopThreshold.current = propAutoscrollToTopThreshold;
102+
minIndexForVisible.current = propMinIndexForVisible;
103+
isMvcpPropPresentRef.current = !!props.maintainVisibleContentPosition;
104+
enableMvcpWithRetriesRef.current();
105+
}
106+
}, [props.maintainVisibleContentPosition]);
107+
108+
const refCallback = useRef<
109+
(instance: RNSectionList<ItemT, SectionT>) => void
110+
>((ref) => {
111+
sectionListRef.current = ref;
112+
enableMvcpWithRetriesRef.current();
113+
if (typeof forwardedRef === 'function') {
114+
forwardedRef(ref);
115+
} else if (forwardedRef) {
116+
forwardedRef.current = ref;
117+
}
118+
}).current;
119+
120+
useEffect(() => {
121+
const disableMvcp = disableMvcpRef.current;
122+
return () => {
123+
// clean up the retry mechanism
124+
if (debounceTimeoutId.current) {
125+
clearTimeout(debounceTimeoutId.current);
126+
}
127+
// clean up any debounce
128+
if (debounceTimeoutId.current) {
129+
clearTimeout(debounceTimeoutId.current);
130+
}
131+
disableMvcp();
132+
};
133+
}, []);
134+
135+
return <RNSectionList<ItemT, SectionT> ref={refCallback} {...props} />;
136+
}
137+
);

src/SectionList.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { SectionList } from 'react-native';
2+
3+
export default SectionList;

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { default as FlatList } from './FlatList';
22
export { default as ScrollView } from './ScrollView';
3+
export { default as SectionList } from './SectionList';

0 commit comments

Comments
 (0)