Created
September 10, 2020 18:15
-
-
Save likern/9bc003967a6dca5a9d791aebce0d9898 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| import React, { | |
| useCallback, | |
| useEffect, | |
| useState, | |
| FunctionComponent, | |
| ComponentClass, | |
| useMemo, | |
| useRef, | |
| ReactNode | |
| } from 'react'; | |
| import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated'; | |
| import { View, StyleSheet, ViewStyle, StyleProp, Text } from 'react-native'; | |
| type Operation = 'round'; | |
| type Precision = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; | |
| const isRound = (num: number, precision: Precision) => { | |
| 'worklet'; | |
| //return decimalPlaces >= 0 && | |
| // +num.toFixed(decimalPlaces) === num; | |
| var p = Math.pow(10, precision); | |
| return Math.round(num * p) / p === num; | |
| }; | |
| const decimalAdjust = (type: Operation, num: number, precision: Precision) => { | |
| 'worklet'; | |
| if (isRound(num, precision)) return num; | |
| var p = Math.pow(10, precision); | |
| var e = Number.EPSILON * num * p; | |
| return Math[type](num * p + e) / p; | |
| }; | |
| const round = (num: number, precision: Precision) => { | |
| 'worklet'; | |
| return decimalAdjust('round', num, precision); | |
| }; | |
| const areEqual = (num1: number, num2: number, precision: Precision) => { | |
| 'worklet'; | |
| return round(num1, precision) === round(num2, precision); | |
| }; | |
| type ComponentType<P> = FunctionComponent<P> | ComponentClass<P>; | |
| type RowType<Key, Props extends {}> = [Key, Props, ComponentType<Props>]; | |
| export interface StringConvertable { | |
| toString(): string; | |
| } | |
| type InfiniteListProps<Key, Props> = { | |
| style?: StyleProp<ViewStyle>; | |
| nextPage(lastRow: RowType<Key, Props> | null): Promise<RowType<Key, Props>[]>; | |
| }; | |
| const createMap = <Key, Value>() => { | |
| return new Map<Key, Value>(); | |
| }; | |
| const createEmptyObject = () => ({}); | |
| interface PageProps { | |
| children?: ReactNode; | |
| style?: StyleProp<ViewStyle>; | |
| } | |
| const Page = (props: PageProps) => { | |
| return ( | |
| <Animated.View style={[styles.page, props.style]}> | |
| {props.children} | |
| </Animated.View> | |
| ); | |
| }; | |
| const InfiniteList = <Key extends StringConvertable | string | number, Props>({ | |
| style, | |
| nextPage | |
| }: InfiniteListProps<Key, Props>) => { | |
| const [internalState, setInternalState] = useState(createEmptyObject); | |
| const rerender = useCallback(() => { | |
| setInternalState({}); | |
| }, [setInternalState]); | |
| const [pageCache] = useState<Map<number, React.ReactElement<any>>>(createMap); | |
| const lastRowRef = useRef<RowType<Key, Props> | null>(null); | |
| const lastPageRef = useRef<number | null>(null); | |
| const addNextPageToCache = useCallback(async () => { | |
| const rows = await nextPage(lastRowRef.current); | |
| const rowsLength = rows.length; | |
| if (rowsLength > 0) { | |
| const elements: React.ReactElement<Props>[] = new Array(rowsLength); | |
| for (let i = 0; i < rowsLength; i++) { | |
| const [key, props, component] = rows[i]; | |
| let elementKey: string | number = | |
| typeof key !== 'number' && typeof key !== 'string' | |
| ? key.toString() | |
| : key; | |
| const finalProps = { | |
| key: elementKey, | |
| ...props | |
| }; | |
| const element = React.createElement(component, finalProps); | |
| elements[i] = element; | |
| if (i + 1 === rowsLength) { | |
| lastRowRef.current = rows[i]; | |
| } | |
| } | |
| const pageKey = (lastPageRef.current ?? 0) + 1; | |
| lastPageRef.current = pageKey; | |
| const page = React.createElement( | |
| Page, | |
| { | |
| key: pageKey | |
| }, | |
| elements | |
| ); | |
| pageCache.set(pageKey, page); | |
| } | |
| }, [pageCache, nextPage]); | |
| useEffect(() => { | |
| (async () => { | |
| if (lastPageRef.current === null) { | |
| await addNextPageToCache(); | |
| rerender(); | |
| } | |
| })(); | |
| }, [addNextPageToCache, rerender]); | |
| const onTopReached = useCallback(() => {}, []); | |
| const onEndReached = useCallback(() => { | |
| (async () => { | |
| await addNextPageToCache(); | |
| rerender(); | |
| })(); | |
| }, [rerender, addNextPageToCache]); | |
| const scrollHandler = useAnimatedScrollHandler({ | |
| onScroll: (event) => { | |
| const isTopReached = areEqual(event.contentOffset.y, 0, 2); | |
| if (isTopReached) { | |
| onTopReached && onTopReached(); | |
| } else { | |
| const isEndReached = areEqual( | |
| event.layoutMeasurement.height + event.contentOffset.y, | |
| event.contentSize.height, | |
| 2 | |
| ); | |
| if (isEndReached) { | |
| onEndReached && onEndReached(); | |
| } | |
| } | |
| }, | |
| onBeginDrag: (_event) => {}, | |
| onEndDrag: (_event) => {}, | |
| onMomentumBegin: (_event) => {}, | |
| onMomentumEnd: (_event) => {} | |
| }); | |
| const children = useMemo(() => { | |
| console.log(`useMemo called, cache size: ${pageCache.size}`); | |
| const pages: React.ReactElement<PageProps>[] = []; | |
| for (const element of pageCache.values()) { | |
| pages.push(element); | |
| } | |
| return pages; | |
| }, [internalState, pageCache]); | |
| return ( | |
| <Animated.ScrollView | |
| style={[{ width: '100%', height: '100%' }, style]} | |
| onScroll={scrollHandler} | |
| > | |
| {children} | |
| </Animated.ScrollView> | |
| ); | |
| }; | |
| const styles = StyleSheet.create({ | |
| page: { | |
| width: '100%', | |
| height: '100%' | |
| } | |
| }); | |
| interface TextRowProps { | |
| id: number; | |
| title: string; | |
| style?: StyleProp<ViewStyle>; | |
| } | |
| const TextRow = (props: TextRowProps) => { | |
| return ( | |
| <View | |
| key={props.id} | |
| style={[ | |
| { minHeight: 56, alignItems: 'center', justifyContent: 'center' }, | |
| props.style | |
| ]} | |
| > | |
| <Text style={{ fontSize: 18 }}>{props.title}</Text> | |
| </View> | |
| ); | |
| }; | |
| const nextPage = async (lastRow: RowType<number, TextRowProps> | null) => { | |
| const lastRowId: number = lastRow?.[0] ?? 0; | |
| const count = 15; | |
| const rows: RowType<StringConvertable, TextRowProps>[] = []; | |
| for (let i = 1; i <= count; i++) { | |
| const id = i + lastRowId; | |
| const taskProps: TextRowProps = { | |
| id, | |
| title: `Task ${id}` | |
| }; | |
| const row: RowType<StringConvertable, TextRowProps> = [ | |
| id, | |
| taskProps, | |
| TextRow | |
| ]; | |
| rows.push(row); | |
| } | |
| return rows; | |
| }; | |
| export const TestInfiniteList = () => { | |
| return <InfiniteList nextPage={nextPage} />; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment