Created
November 26, 2024 17:13
-
-
Save vanGalilea/9bde124e4bf9af53ef54390632ae7325 to your computer and use it in GitHub Desktop.
A custom FlashList in React Native with top and bottom fading gradients that adjust dynamically based on scroll position for enhanced UX.
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 { FlashList } from "@shopify/flash-list"; | |
import { FlashListProps } from "@shopify/flash-list/src/FlashListProps"; | |
import { LinearGradient } from "expo-linear-gradient"; | |
import React, { useCallback, useState } from "react"; | |
import { StyleSheet, View, ViewStyle } from "react-native"; | |
import { useSafeAreaInsets } from "react-native-safe-area-context"; | |
import { NativeScrollEvent } from "react-native/Libraries/Components/ScrollView/ScrollView"; | |
import { | |
LayoutChangeEvent, | |
NativeSyntheticEvent, | |
} from "react-native/Libraries/Types/CoreEventTypes"; | |
type FadeFlashListProps<T> = { | |
containerStyle?: ViewStyle; | |
} & FlashListProps<T>; | |
/** | |
* A FlashList with fading gradients at the top and bottom. | |
* The gradients will fade in and out based on the scroll position. | |
*/ | |
export default <T,>({ containerStyle, ...props }: FadeFlashListProps<T>) => { | |
const [showTopFade, setShowTopFade] = useState(false); | |
const [showBottomFade, setShowBottomFade] = useState(true); | |
const [contentHeight, setContentHeight] = useState(0); | |
const [containerHeight, setContainerHeight] = useState(0); | |
const isFadeDisabled = contentHeight <= containerHeight; | |
const insets = useSafeAreaInsets(); | |
const handleScroll = useCallback( | |
(event: NativeSyntheticEvent<NativeScrollEvent>) => { | |
if (isFadeDisabled) return; | |
const { contentOffset, layoutMeasurement, contentSize } = | |
event.nativeEvent; | |
const isTop = contentOffset.y <= 0; | |
const itemMargin = props.estimatedItemSize | |
? props.estimatedItemSize / 2 | |
: 0; | |
const isBottom = | |
contentOffset.y + layoutMeasurement.height >= | |
contentSize.height - itemMargin; | |
setShowTopFade(!isTop); | |
setShowBottomFade(!isBottom); | |
}, | |
[isFadeDisabled, props.estimatedItemSize], | |
); | |
const handleOnLayout = useCallback((event: LayoutChangeEvent) => { | |
const { height } = event.nativeEvent.layout; | |
setContainerHeight(height); | |
}, []); | |
const onContentSizeChange = useCallback((_: number, height: number) => { | |
setContentHeight(height); | |
}, []); | |
return ( | |
<View style={containerStyle} onLayout={handleOnLayout}> | |
{showTopFade && ( | |
<LinearGradient | |
pointerEvents="none" | |
colors={["transparent", "rgba(0,0,0,.25)", "transparent"]} | |
locations={[0, 0.25, 1]} | |
style={styles.topGradient} | |
/> | |
)} | |
<FlashList | |
{...props} | |
onScroll={handleScroll} | |
onContentSizeChange={onContentSizeChange} | |
/> | |
{showBottomFade && ( | |
<LinearGradient | |
pointerEvents="none" | |
colors={["transparent", "#000"]} | |
style={[styles.bottomGradient]} | |
/> | |
)} | |
</View> | |
); | |
}; | |
const GRADIENT_HEIGHT_M = 100; | |
const GRADIENT_HEIGHT_L = 250; | |
const styles = StyleSheet.create({ | |
topGradient: { | |
position: "absolute", | |
top: -25, | |
left: 0, | |
right: 0, | |
height: GRADIENT_HEIGHT_M, | |
zIndex: 5, | |
}, | |
bottomGradient: { | |
position: "absolute", | |
left: 0, | |
right: 0, | |
bottom: 0, | |
height: GRADIENT_HEIGHT_L, | |
zIndex: 5, | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great timing! I was just looking for something like this and actually had started down this same path. This looks great, thank you! I haven't loaded this into my IDE yet but is
insets
used at all? Also, what's the significance of thetop: -25
, specifically the 25? I wonder if you could turn that into a descriptively named variable so it's not a magic number.