Skip to content

Instantly share code, notes, and snippets.

@likern
Created September 12, 2020 21:13
Show Gist options
  • Save likern/e38c9668895ee0d0be58b61eca574a6c to your computer and use it in GitHub Desktop.
Save likern/e38c9668895ee0d0be58b61eca574a6c to your computer and use it in GitHub Desktop.
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