Skip to content

Instantly share code, notes, and snippets.

@vanGalilea
Created November 26, 2024 17:13
Show Gist options
  • Save vanGalilea/9bde124e4bf9af53ef54390632ae7325 to your computer and use it in GitHub Desktop.
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.
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,
},
});
@bradydowling
Copy link

bradydowling commented Dec 3, 2024

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 the top: -25, specifically the 25? I wonder if you could turn that into a descriptively named variable so it's not a magic number.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment