Skip to content

Commit

Permalink
feat: hold position when removing data at index 0
Browse files Browse the repository at this point in the history
  • Loading branch information
friedolinfoerder committed Aug 25, 2022
1 parent d0325c3 commit 2cbff16
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ public void setShiftOffset(double shiftOffset) {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
super.onLayoutChange(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom);
int shiftHeight = Math.min(bottom - oldBottom, (int)mShiftHeight);
int scrollWindowHeight = getHeight() - getPaddingBottom() - getPaddingTop();
if(mShiftHeight != 0 && shiftHeight > 0 && mShiftOffset <= getScrollY() + scrollWindowHeight / 2) {
if(mShiftHeight != 0 && mShiftOffset <= getScrollY() + scrollWindowHeight / 2) {
// correct
scrollTo(0, getScrollY() + shiftHeight);
scrollTo(0, getScrollY() + (int)mShiftHeight);
if(getOverScrollerFromParent() != null && !getOverScrollerFromParent().isFinished()) {
// get current directed velocity from scroller
int direction = getOverScrollerFromParent().getFinalY() - getOverScrollerFromParent().getStartY() > 0 ? 1 : -1;
Expand Down
22 changes: 20 additions & 2 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const Message: FC<MessageType> = ({ id, color, height }) => {
};


const types = ['FlatList', 'AnimatedFlatList'] as const;
const types = ['FlatList', 'AnimatedFlatList', 'getItemLayout'] as const;
type Type = typeof types[number];

const ExampleLink = ({type}: {type: Type}) => {
Expand All @@ -92,8 +92,12 @@ const Example = ({children}: {children: (props: any) => ReactNode}) => {

const keyExtractor = useCallback((item) => item.id, []);

const removeFirst = useCallback(async () => {
setData((x) => x.slice(1));
}, []);

const prependAndRemoveFirst = useCallback(async () => {
const newData = generateData(20);
const newData = generateData(5);
setData((x) => [...newData, ...x.slice(1)]);
}, []);

Expand Down Expand Up @@ -134,6 +138,7 @@ const Example = ({children}: {children: (props: any) => ReactNode}) => {
justifyContent: 'center',
}}
>
<Button title="Remove First" onPress={removeFirst} />
<Button title="Prepend And Remove First" onPress={prependAndRemoveFirst} />
<Button title="Prepend" onPress={prepend} />
<Button title="Append" onPress={append} />
Expand Down Expand Up @@ -171,6 +176,18 @@ const Example = ({children}: {children: (props: any) => ReactNode}) => {
</ExampleContext.Consumer>
}

const GetItemLayoutExample = (props: any) => {
const getItemLayout = (data: MessageType[], index: number) => {
return {
index,
length: data[index]?.height,
offset: data.slice(0, index).reduce((p,c) => p + c.height, 0),
}
}

return <FlatListReanimated {...props} getItemLayout={getItemLayout} />
}

export default function App() {
const [type, setType] = useState<Type>();

Expand All @@ -184,6 +201,7 @@ export default function App() {
</View>}
{type === 'FlatList' && <Example>{props => <BidirectionalFlatList {...props} />}</Example>}
{type === 'AnimatedFlatList' && <Example>{props => <FlatListReanimated {...props} />}</Example>}
{type === 'getItemLayout' && <Example>{props => <GetItemLayoutExample {...props} />}</Example>}
</View>
</ExampleContext.Provider>
);
Expand Down
13 changes: 7 additions & 6 deletions src/FlatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@ const FlatListImpl = forwardRef<FlatListType, BidirectionalFlatListProps>((props
} else {
(ref as MutableRefObject<FlatListType>).current = obj;
}
}, []);
}, [ref]);

// todo add hack to prevent flickering
const {
data = [],
keyExtractor = (item) => item.id,
keyExtractor = (item: any) => item.id ?? item.key,
renderItem,
onUpdateData,
getItemLayout,
onLayout,
} = props;
const {finalData, prerender, getItemLayoutCustom} = usePrerenderedData({
data: data ?? [],
Expand All @@ -51,18 +52,18 @@ const FlatListImpl = forwardRef<FlatListType, BidirectionalFlatListProps>((props
});

const [width, setWidth] = useState<number>();
const onLayout = useCallback((e: LayoutChangeEvent) => {
props.onLayout?.(e);
const onLayoutFlatList = useCallback((e: LayoutChangeEvent) => {
onLayout?.(e);
setWidth(e.nativeEvent.layout.width);
}, []);
}, [onLayout]);

return <>
<FlatListRN
maintainVisibleContentPosition={props.maintainVisibleContentPosition ?? maintainVisibleContentPosition}
renderScrollComponent={Platform.OS === 'android' ? renderScrollComponent : undefined}
getItemLayout={getItemLayoutCustom}
{...props}
onLayout={onLayout}
onLayout={onLayoutFlatList}
data={finalData}
ref={captureRef} />
{prerender && !!width && <View style={[styles.prerender, {width}]}>
Expand Down
87 changes: 52 additions & 35 deletions src/hooks/usePrerenderedData.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { MutableRefObject, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { FlatListProps, LayoutChangeEvent, View } from 'react-native';
import type { FlatListType, OnUpdateData, RenderItem } from '../types';
import type { FlatListType, KeyExtractor, OnUpdateData, RenderItem } from '../types';
import { MIN_INDEX } from '../config';

type OnLayout = (options: {id: string; height: number}) => void;
Expand All @@ -11,7 +11,7 @@ const Prerender = ({id, onLayout, children}: {id: string; onLayout: OnLayout; ch
id,
height: e.nativeEvent.layout.height,
});
}, []);
}, [id, onLayout]);

return <View onLayout={onLayoutView}>
{children}
Expand All @@ -20,7 +20,7 @@ const Prerender = ({id, onLayout, children}: {id: string; onLayout: OnLayout; ch

export const usePrerenderedData = ({data, keyExtractor, renderItem, scrollRef, onUpdateData, getItemLayout}: {
data: readonly any[];
keyExtractor: Required<FlatListProps<any>>['keyExtractor'];
keyExtractor: KeyExtractor;
renderItem: RenderItem
scrollRef: MutableRefObject<FlatListType>;
onUpdateData?: OnUpdateData;
Expand All @@ -30,31 +30,52 @@ export const usePrerenderedData = ({data, keyExtractor, renderItem, scrollRef, o
const [newData, setNewData] = useState<any[]>([]);

const heightsRef = useRef<Record<string, any>>({});
const getHeight = useCallback((list: any[], heights= heightsRef.current) => {
return list.reduce((p, c) => p + heights[keyExtractor(c)], 0)
}, [keyExtractor]);

const shift = useCallback((newData: any[], oldHeights: Record<string, number>) => {
const removedData = [];
for (const d of finalData) {
if(!heightsRef.current[keyExtractor(d)]) {
removedData.push(d);
} else {
break;
}
}

const shiftValue = {
height: -getHeight(removedData, oldHeights),
offset: 0,
};
const index = data.findIndex((d) => d === newData[0]);
if(index >= 0 && index <= MIN_INDEX) {
shiftValue.height += getHeight(newData);
shiftValue.offset = getHeight(data.slice(0, index))
}
if(shiftValue.height !== 0) {
scrollRef.current?.shift(shiftValue);
onUpdateData?.({heights: heightsRef.current, ...shiftValue});
}
setFinalData(data);
}, [data, finalData, getHeight, keyExtractor, onUpdateData, scrollRef]);

useEffect(() => {
if(data === finalData) {
return;
}
if(getItemLayout) {
const newD = data.filter((d, i) => !heightsRef.current[keyExtractor(d, i)]);
const newD = data.filter((d) => !heightsRef.current[keyExtractor(d)]);
const oldHeights = heightsRef.current;
heightsRef.current = data.reduce((p,c,i) => {
p[keyExtractor(c, i)] = getItemLayout(data as any[], i).length;
p[keyExtractor(c)] = getItemLayout(data as any[], i).length;
return p;
}, {});
const index = data.findIndex((d) => d === newD[0]);
if(index >= 0 && index <= MIN_INDEX) {
const shift = {
height: newD.reduce((p, c, i) => p + heightsRef.current[keyExtractor(c, i)], 0),
offset: data.slice(0, index).reduce((p, c, i) => p + heightsRef.current[keyExtractor(c, i)], 0),
};
scrollRef.current?.shift(shift);
onUpdateData?.({heights: heightsRef.current, ...shift});
}
setFinalData(data);
shift(newD, oldHeights);
return;
}
setNewData(data.filter((d, i) => !heightsRef.current[keyExtractor(d, i)]));
}, [data, finalData, getItemLayout, keyExtractor, onUpdateData, scrollRef]);
setNewData(data.filter((d) => !heightsRef.current[keyExtractor(d)]));
}, [data, finalData, getHeight, getItemLayout, keyExtractor, onUpdateData, scrollRef, shift]);

useEffect(() => {
if(getItemLayout || !newData.length) {
Expand All @@ -65,38 +86,34 @@ export const usePrerenderedData = ({data, keyExtractor, renderItem, scrollRef, o
const onLayout = useCallback<OnLayout>(({id, height}) => {
heightsRef.current[id] = height;
// check if there are missing elements
const missing = data.some((d, i) => heightsRef.current[keyExtractor(d, i)] === undefined);
const missing = data.some((d) => heightsRef.current[keyExtractor(d)] === undefined);
if(missing) {
return;
}
const index = data.findIndex((d) => d === newData[0]);
if(index >= 0 && index <= MIN_INDEX) {
const shift = {
height: newData.reduce((p, c, i) => p + heightsRef.current[keyExtractor(c, i)], 0),
offset: data.slice(0, index).reduce((p, c, i) => p + heightsRef.current[keyExtractor(c, i)], 0),
};
scrollRef.current?.shift(shift);
onUpdateData?.({heights: heightsRef.current, ...shift});
}
const oldHeights = heightsRef.current;
// clean current heights (=> remove old heights)
heightsRef.current = data.reduce((p,c) => {
const id = keyExtractor(c);
p[id] = oldHeights[id];
return p;
}, {});
shift(newData, oldHeights);
setNewData([]);
setFinalData(data);
}, [data, keyExtractor, newData, onUpdateData, scrollRef]);
}, [data, keyExtractor, newData, shift]);

return {
finalData,
prerender: newData.length ? <View>
{newData.map((d, i) => <Prerender key={keyExtractor(d, i)} id={keyExtractor(d, i)} onLayout={onLayout}>{renderItem({item: d, prerendering: true})}</Prerender>)}
{newData.map((d) => <Prerender key={keyExtractor(d)} id={keyExtractor(d)} onLayout={onLayout}>{renderItem({item: d, prerendering: true})}</Prerender>)}
</View> : undefined,
getItemLayoutCustom: useCallback((data, index) => {
const d = data[index];
const id = keyExtractor(d, index);
const id = keyExtractor(d);
return {
index,
length: heightsRef.current[id],
offset: data.slice(0, index).reduce((p: number, c: any, i: number) => {
return p + heightsRef.current[keyExtractor(c, i)];
}, 0),
offset: getHeight(data.slice(0, index)),
}
}, [keyExtractor]),
}, [getHeight, keyExtractor]),
};
};
5 changes: 4 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ export type ShiftFunction = ({ offset, height }: { offset: number; height: numbe

export type FlatListType = typeof FlatListRN & {shift: ShiftFunction};

export type BidirectionalFlatListProps = FlatListProps<any> & {renderItem: RenderItem, onUpdateData?: OnUpdateData};
export type KeyExtractor = <T>(item: T) => string;

export type BidirectionalFlatListProps = FlatListProps<any> & {renderItem: RenderItem, onUpdateData?: OnUpdateData, keyExtractor: KeyExtractor};


0 comments on commit 2cbff16

Please sign in to comment.