Created
September 12, 2020 21:13
-
-
Save likern/e38c9668895ee0d0be58b61eca574a6c 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, | |
Key | |
} from 'react'; | |
import Animated, { | |
measure, | |
scrollTo, | |
useAnimatedRef, | |
useAnimatedScrollHandler, | |
useSharedValue | |
} from 'react-native-reanimated'; | |
import { View, 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<K, P extends {}> = [K, P, ComponentType<P>]; | |
export interface StringConvertable { | |
toString(): string; | |
} | |
type InfiniteListProps<K, P> = { | |
style?: StyleProp<ViewStyle>; | |
nextPage(lastRow: RowType<K, P> | null): Promise<RowType<K, P>[]>; | |
}; | |
const createMap = <Key, Value>() => { | |
return new Map<Key, Value>(); | |
}; | |
const createEmptyObject = () => ({}); | |
interface TrackedRowProps {} | |
const TrackedRow = React.forwardRef<Animated.View, TrackedRowProps>( | |
(props, ref) => { | |
return <Animated.View ref={ref}>{props.children}</Animated.View>; | |
} | |
); | |
const InfiniteList = <Props,>({ | |
style, | |
nextPage | |
}: InfiniteListProps<Key, Props>) => { | |
const [internalState, setInternalState] = useState(createEmptyObject); | |
const rerender = useCallback(() => { | |
setInternalState({}); | |
}, [setInternalState]); | |
const [rows] = useState<Map<Key, React.ReactElement<any>>>(createMap); | |
const [pages] = useState<Map<number, Key[]>>(createMap); | |
const pageIdsRef = useRef<number[]>([]); | |
const firstPageIsDeleting: Animated.SharedValue<boolean> = useSharedValue( | |
false | |
); | |
const scrollViewRef = useAnimatedRef<Animated.ScrollView>(); | |
const firstPageFirstRowRef = useAnimatedRef<Animated.View>(); | |
const firstPageLastRowRef = useAnimatedRef<Animated.View>(); | |
const lastPageFirstRowRef = useAnimatedRef<Animated.View>(); | |
const lastPageLastRowRef = useAnimatedRef<Animated.View>(); | |
const lastPageFirstRowRefData = useRef<RowType<Key, Props> | null>(null); | |
const lastPageLastRowRefData = useRef<RowType<Key, Props> | null>(null); | |
const lastPageRef = useRef<number | null>(null); | |
const deleteRowsOfFirstPage = useCallback(() => { | |
console.log('onFirstPageDelete: function start'); | |
if (pageIdsRef.current.length > 0) { | |
const pageId = pageIdsRef.current.shift(); | |
if (pageId !== undefined) { | |
const pageRows = pages.get(pageId); | |
if (pageRows !== undefined) { | |
pageRows.forEach((pageRow) => { | |
rows.delete(pageRow); | |
}); | |
} | |
} | |
rerender(); | |
} | |
}, [rows, pages, pageIdsRef, rerender]); | |
const addNextPageToCache = useCallback(async () => { | |
const data = await nextPage(lastPageLastRowRefData.current); | |
const length = data.length; | |
if (length > 0) { | |
const pageId = pages.size + 1; | |
const rowsKeys: Key[] = Array(length); | |
for (let i = 0; i < length; i++) { | |
const isFirstRow = i === 0; | |
const isLastRow = i + 1 === length; | |
const isFirstOrLastRow = isFirstRow || isLastRow; | |
const [key, props, component] = data[i]; | |
const finalProps: { | |
key: React.ReactText; | |
} & Props = { | |
...props, | |
key | |
}; | |
let row: JSX.Element = React.createElement(component, finalProps); | |
if (isFirstOrLastRow) { | |
let ref: React.RefObject<Animated.View> | undefined; | |
if (pages.size === 0) { | |
if (isFirstRow) { | |
ref = firstPageFirstRowRef; | |
} else if (isLastRow) { | |
ref = firstPageLastRowRef; | |
} | |
} else { | |
if (isFirstRow) { | |
ref = lastPageFirstRowRef; | |
lastPageFirstRowRefData.current = data[i]; | |
} else if (isLastRow) { | |
ref = lastPageLastRowRef; | |
lastPageLastRowRefData.current = data[i]; | |
} | |
} | |
const trackedRow = React.createElement( | |
TrackedRow, | |
{ | |
key, | |
ref | |
}, | |
row | |
); | |
row = trackedRow; | |
} | |
rows.set(key, row); | |
rowsKeys[i] = key; | |
} | |
console.log(`add to page with id ${pageId}`); | |
pages.set(pageId, rowsKeys); | |
pageIdsRef.current.push(pageId); | |
} | |
}, [ | |
pages, | |
rows, | |
nextPage, | |
lastPageFirstRowRef, | |
lastPageLastRowRef, | |
firstPageFirstRowRef, | |
firstPageLastRowRef | |
]); | |
useEffect(() => { | |
(async () => { | |
if (lastPageRef.current === null) { | |
await addNextPageToCache(); | |
rerender(); | |
} | |
})(); | |
}, [addNextPageToCache, rerender]); | |
const onTopReached = useCallback(() => {}, []); | |
const onEndReached = useCallback(() => { | |
console.log('onEndReached: start function'); | |
(async () => { | |
await addNextPageToCache(); | |
rerender(); | |
})(); | |
}, [rerender, addNextPageToCache]); | |
const scrollHandler = useAnimatedScrollHandler({ | |
onScroll: (event) => { | |
console.log(`onScroll: event: ${JSON.stringify(event)}`); | |
const firstPageLastRowResult = measure(firstPageLastRowRef); | |
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(); | |
} else if ( | |
firstPageLastRowResult.pageY + firstPageLastRowResult.height <= | |
0 | |
) { | |
if (firstPageIsDeleting.value === false) { | |
firstPageIsDeleting.value = true; | |
deleteRowsOfFirstPage && deleteRowsOfFirstPage(); | |
scrollTo(scrollViewRef, 0, 0, false); | |
} | |
} | |
} | |
}, | |
onBeginDrag: (_event) => {}, | |
onEndDrag: (_event) => {}, | |
onMomentumBegin: (_event) => {}, | |
onMomentumEnd: (_event) => {} | |
}); | |
const children = useMemo(() => { | |
console.log(`useMemo called, cache size: ${rows.size}`); | |
const elements: JSX.Element[] = []; | |
for (const element of rows.values()) { | |
elements.push(element); | |
} | |
return elements; | |
}, [internalState, rows]); | |
return ( | |
<Animated.ScrollView | |
ref={scrollViewRef} | |
style={[{ width: '100%', height: '100%' }, style]} | |
onScroll={scrollHandler} | |
> | |
{children} | |
</Animated.ScrollView> | |
); | |
}; | |
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 = 14; | |
console.log(`nextPage: create ${count} rows`); | |
const rows: RowType<Key, TextRowProps>[] = []; | |
for (let i = 1; i <= count; i++) { | |
const id = i + lastRowId; | |
const taskProps: TextRowProps = { | |
id, | |
title: `Task ${id}`, | |
style: { | |
backgroundColor: i % 2 === 0 ? '#FAF0CA' : '#EE964B' | |
} | |
}; | |
const row: RowType<Key, 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