Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy flatlist #46

Merged
merged 3 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions example/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default function Layout() {
name="scrollviews/vertical"
options={{ title: 'Vertical Lazy' }}
/>
<Stack.Screen
name="scrollviews/flatlist"
options={{ title: 'Lazy FlatList' }}
/>
<Stack.Screen
name="scrollviews/horizontal"
options={{ title: 'Horizontal Lazy' }}
Expand Down
2 changes: 2 additions & 0 deletions example/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { HorizontalCard } from '../components/cards/HorizontalCard';
import { NoLazyCard } from '../components/cards/NoLazyCard';
import { VerticalCard } from '../components/cards/VerticalCard';
import { FlatListCard } from '../components/cards/FlatListCard';

export default function App() {
const { top, bottom } = useSafeAreaInsets();
Expand All @@ -32,6 +33,7 @@ export default function App() {
</View>
<VerticalCard />
<HorizontalCard />
<FlatListCard />
<NoLazyCard />
</ScrollView>
);
Expand Down
124 changes: 124 additions & 0 deletions example/app/scrollviews/flatlist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useCallback, useRef } from 'react';
import {
ImageSourcePropType,
ListRenderItemInfo,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import {
LazyFlatList,
LazyFlatListMethods,
} from 'react-native-lazy-scrollview';
import { ALBUMS, PADDING_VERTICAL } from '../../constants';
import shuffle from 'lodash/shuffle';
import { FireOnceBlock } from '../../components/blocks/FireOnceBlock';
import { ImageBlock } from '../../components/blocks/ImageBlock';
import { NoLazyChild } from '../../components/blocks/NoLazyChild';
import { Header } from '../../components/blocks/Header';

const OFFSET = -50;

type Block = ImageSourcePropType | 'no-lazy' | 'fire-once';

export default function FlatList() {
const ref = useRef<LazyFlatListMethods>(null);

const data: Block[] = shuffle(
ALBUMS.concat(shuffle(ALBUMS)).concat(shuffle(ALBUMS))
);

const renderItem = useCallback(
({ item: source, index }: ListRenderItemInfo<Block>) => {
if (source === 'no-lazy') {
return <NoLazyChild key={`no-lazy-child-${index}`} />;
}

if (source === 'fire-once') {
return (
<FireOnceBlock
key={`fire-once-child-${index}`}
percentVisibleThreshold={1}
debug
/>
);
}

return (
<ImageBlock
key={`${source.toString()}-${index}`}
source={source}
percentVisibleThreshold={1}
debug
/>
);
},
[]
);

return (
<View style={styles.scrollviewContainer}>
<LazyFlatList
ref={ref}
offset={OFFSET}
showsVerticalScrollIndicator={false}
numColumns={2}
ListHeaderComponent={Header}
data={data}
renderItem={renderItem}
debug
/>
<View style={styles.arrowsContainer}>
<TouchableOpacity
style={styles.arrowButton}
activeOpacity={0.7}
onPress={() => ref.current?.scrollToStart({ animated: true })}
>
<Text style={styles.arrow}>⬆️</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => ref.current?.scrollToEnd({ animated: true })}
>
<Text style={styles.arrow}>⬇️</Text>
</TouchableOpacity>
</View>
</View>
);
}

const styles = StyleSheet.create({
scrollviewContainer: {
flex: 1,
backgroundColor: '#ecf0f1',
},
offsetBar: {
position: 'absolute',
bottom: OFFSET * -1 + PADDING_VERTICAL,
borderBottomWidth: 1,
borderBottomColor: 'black',
left: 0,
right: 0,
opacity: 0.7,
height: 50,
justifyContent: 'flex-end',
},
offsetText: {
color: 'white',
fontSize: 18,
fontWeight: '600',
backgroundColor: '#000',
padding: 8,
alignSelf: 'flex-start',
},
arrowsContainer: {
top: 8,
right: 8,
position: 'absolute',
justifyContent: 'center',
flexDirection: 'row',
},
arrowButton: { marginBottom: 8 },
arrow: { fontSize: 32 },
});
Binary file added example/assets/flatlist.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added example/assets/header.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 86 additions & 0 deletions example/components/blocks/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { ComponentProps, useState } from 'react';

Check failure on line 1 in example/components/blocks/Header.tsx

View workflow job for this annotation

GitHub Actions / lint

'ComponentProps' is defined but never used
import {
Dimensions,
Image,
ImageSourcePropType,

Check failure on line 5 in example/components/blocks/Header.tsx

View workflow job for this annotation

GitHub Actions / lint

'ImageSourcePropType' is defined but never used
StyleSheet,
View,
} from 'react-native';
import { LazyChild } from 'react-native-lazy-scrollview';
import { SQUARE_SIZE } from '../../constants';

Check failure on line 10 in example/components/blocks/Header.tsx

View workflow job for this annotation

GitHub Actions / lint

'SQUARE_SIZE' is defined but never used
import { Skeleton } from '../loaders/Skeleton';

const SCREEN_WIDTH = Dimensions.get('window').width;

export function Header() {
const [triggered, setTriggered] = useState(false);
const [isVisible, setIsVisible] = useState(false);

const onEnterThresholdPass = () => {
setTriggered(true);
};

const onExitThresholdPass = () => {
setTriggered(false);
};

const onVisibilityEnter = () => {
setIsVisible(true);
};

const onVisibilityExit = () => {
setIsVisible(false);
};

return (
<View style={styles.container}>
<LazyChild
onEnterThresholdPass={onEnterThresholdPass}
onExitThresholdPass={onExitThresholdPass}
onVisibilityEnter={onVisibilityEnter}
onVisibilityExit={onVisibilityExit}
percentVisibleThreshold={0.5}
>
<View style={styles.contentContainer}>
<Skeleton show={!triggered}>
<Image
source={require('../../assets/header.jpg')}
style={styles.image}
/>
</Skeleton>
{!isVisible ? <View style={styles.percentTextWrapper} /> : null}
</View>
</LazyChild>
</View>
);
}

const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
alignSelf: 'center',
width: SCREEN_WIDTH,
},
contentContainer: {
width: SCREEN_WIDTH,
height: SCREEN_WIDTH * 1.25,
justifyContent: 'center',
alignItems: 'center',
},
image: {
width: SCREEN_WIDTH,
height: SCREEN_WIDTH * 1.25,
resizeMode: 'cover',
},
percentTextWrapper: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: 'rgba(255, 255, 255, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
});
29 changes: 29 additions & 0 deletions example/components/cards/FlatListCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useEffect } from 'react';
import {
interpolate,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
import { FLATLIST } from '../../constants';
import { Card } from './Card';

export function FlatListCard() {
const animation = useSharedValue(0);

useEffect(() => {
animation.value = withRepeat(withTiming(1, { duration: 1000 }), -1, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const animatedStyle = useAnimatedStyle(() => {
return {
marginRight: 16,
transform: [
{ translateY: interpolate(animation.value, [0, 1], [-20, 20]) },
],
};
});

return <Card scrollView={FLATLIST} animatedStyle={animatedStyle} />;
}
9 changes: 9 additions & 0 deletions example/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ export const VERTICAL = {
image: require('./assets/vertical.png'),
};

export const FLATLIST = {
name: 'flatlist',
title: 'Lazy FlatList',
color: '#f8a5c2',
description:
'Less use cases than LazyScrollView, but still useful for situations where you need to measure headers or footers, or have specific cells react granularly to scroll visibility. Not recommended to wrap every item in LazyChild.',
image: require('./assets/flatlist.png'),
};

export const HORIZONTAL = {
name: 'horizontal',
title: 'Horizontal Lazy ScrollView',
Expand Down
97 changes: 97 additions & 0 deletions src/components/LazyFlatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, {
ForwardedRef,
MutableRefObject,
RefAttributes,
forwardRef,
useImperativeHandle,
} from 'react';
import { FlatList } from 'react-native';
import Animated, { useAnimatedRef } from 'react-native-reanimated';
import { AnimatedContext } from '../context/AnimatedContext';
import { useLazyContextValues } from './useLazyContextValues';

export interface LazyFlatListMethods {
scrollToStart: typeof FlatList.prototype.scrollToEnd;
scrollToEnd: typeof FlatList.prototype.scrollToEnd;
scrollToIndex: typeof FlatList.prototype.scrollToIndex;
scrollToOffset: typeof FlatList.prototype.scrollToOffset;
scrollToItem: typeof FlatList.prototype.scrollToItem;
}

export interface LazyFlatListProps {
/**
* How far above or below the bottom of the FlatList the threshold trigger is. Negative is above, postive it below. Accepts [FlatList props](https://reactnative.dev/docs/FlatList).
* @defaultValue 0 (bottom of Fl)
*/
offset?: number;
/**
* Ref to the LazyFlatList. Exposes scrollTo, scrollToStart, and scrollToEnd methods.
*/
ref?: MutableRefObject<LazyFlatListMethods | null>;
/**
* When true, console.logs FlatList measurements. Even if true, will not call console.log in production.
*/
debug?: boolean;
}

type Props<T> = LazyFlatListProps &
Omit<
React.ComponentProps<typeof Animated.FlatList<T>>,
'onLayout' | 'onScroll' | 'ref'
>;

/**
* LazyFlatList to wrap Lazy Children in.
*/
const UnwrappedLazyFlatList = <T,>(
{ offset: injectedOffset, debug = false, ...rest }: Props<T>,
ref: ForwardedRef<LazyFlatListMethods>
) => {
const _flatListRef = useAnimatedRef<Animated.FlatList<T>>();

useImperativeHandle(ref, () => ({
scrollToStart: (options) => {
_flatListRef.current?.scrollToOffset({
offset: 0,
animated: options?.animated,
});
},
scrollToEnd: (options) => {
_flatListRef.current?.scrollToEnd(options);
},
scrollToIndex: (options) => {
_flatListRef.current?.scrollToIndex(options);
},
scrollToOffset: (options) => {
_flatListRef.current?.scrollToOffset(options);
},
scrollToItem: (options) => {
_flatListRef.current?.scrollToItem(options);
},
}));

const { value, measureScroller } = useLazyContextValues({
offset: injectedOffset,
debug,
horizontal: rest.horizontal,
// @ts-ignore
ref: _flatListRef,
});

return (
<AnimatedContext.Provider value={value}>
<Animated.FlatList
scrollEventThrottle={16}
{...rest}
ref={_flatListRef}
onLayout={measureScroller}
/>
</AnimatedContext.Provider>
);
};

const LazyFlatList = forwardRef(UnwrappedLazyFlatList) as <T>(
props: Props<T> & RefAttributes<LazyFlatListMethods>
) => React.ReactElement;

export { LazyFlatList };
Loading
Loading