-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
291 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} />; | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { SectionList } from 'react-native'; | ||
|
||
export default SectionList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |