diff --git a/example/src/navigation/AppNavigation.tsx b/example/src/navigation/AppNavigation.tsx index 2e4d924..28364bd 100644 --- a/example/src/navigation/AppNavigation.tsx +++ b/example/src/navigation/AppNavigation.tsx @@ -1,7 +1,13 @@ import React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import type { RootStackParamList } from './types'; -import { FlatListUsageScreen, HomeScreen, ProfileScreen, SimpleUsageScreen } from '../screens'; +import { + FlatListUsageScreen, + HomeScreen, + ProfileScreen, + SectionListUsageScreen, + SimpleUsageScreen, +} from '../screens'; const Stack = createNativeStackNavigator(); @@ -15,5 +21,6 @@ export default () => ( /> + ); diff --git a/example/src/navigation/types.ts b/example/src/navigation/types.ts index 27d8fab..557c4f3 100644 --- a/example/src/navigation/types.ts +++ b/example/src/navigation/types.ts @@ -6,6 +6,7 @@ export type RootStackParamList = { Profile: undefined; SimpleUsageScreen: undefined; FlatListUsageScreen: undefined; + SectionListUsageScreen: undefined; }; // Overrides the typing for useNavigation in @react-navigation/native to support the internal @@ -29,3 +30,8 @@ export type FlatListUsageScreenNavigationProps = NativeStackScreenProps< RootStackParamList, 'FlatListUsageScreen' >; + +export type SectionListUsageScreenNavigationProps = NativeStackScreenProps< + RootStackParamList, + 'SectionListUsageScreen' +>; diff --git a/example/src/screens/Home.tsx b/example/src/screens/Home.tsx index dcc29e8..b5f2a58 100644 --- a/example/src/screens/Home.tsx +++ b/example/src/screens/Home.tsx @@ -28,6 +28,11 @@ const SCREEN_LIST_CONFIG: ScreenConfigItem[] = [ route: 'FlatListUsageScreen', description: "A simple example of the library's FlatList.", }, + { + name: 'SectionList Example', + route: 'SectionListUsageScreen', + description: "A simple example of the library's SectionList.", + }, ]; const HeaderComponent: React.FC = ({ showNavBar }) => { diff --git a/example/src/screens/index.ts b/example/src/screens/index.ts index 1ae59a5..60025e7 100644 --- a/example/src/screens/index.ts +++ b/example/src/screens/index.ts @@ -4,3 +4,4 @@ export { default as ProfileScreen } from './Profile'; // Usage screens export { default as SimpleUsageScreen } from './usage/Simple'; export { default as FlatListUsageScreen } from './usage/FlatList'; +export { default as SectionListUsageScreen } from './usage/SectionList'; diff --git a/example/src/screens/usage/SectionList.tsx b/example/src/screens/usage/SectionList.tsx new file mode 100644 index 0000000..f7e5d65 --- /dev/null +++ b/example/src/screens/usage/SectionList.tsx @@ -0,0 +1,130 @@ +import React, { useCallback } from 'react'; +import { SectionListRenderItem, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useNavigation } from '@react-navigation/native'; +import { + Header, + LargeHeader, + ScalingView, + ScrollHeaderProps, + ScrollLargeHeaderProps, + SectionListWithHeaders, +} from '@codeherence/react-native-header'; +import { Avatar, BackButton } from '../../components'; +import { RANDOM_IMAGE_NUM } from '../../constants'; +import type { SectionListUsageScreenNavigationProps } from '../../navigation'; + +const DATA = [ + { + title: 'Main dishes', + data: ['Pizza', 'Burger', 'Risotto', 'Pasta', 'Lasagna'], + }, + { + title: 'Sides', + data: ['French Fries', 'Onion Rings', 'Fried Shrimps', 'Mozzarella Sticks', 'Garlic Bread'], + }, + { + title: 'Drinks', + data: ['Water', 'Coke', 'Beer', 'Wine', 'Mojito', 'Cuba Libre', 'Pina Colada', 'Margarita'], + }, + { + title: 'Desserts', + data: ['Cheese Cake', 'Ice Cream', 'Chocolate Cake', 'Tiramisu', 'Panna Cotta', 'Profiteroles'], + }, +]; + +const HeaderComponent: React.FC = ({ showNavBar }) => { + const navigation = useNavigation(); + const onPressProfile = () => navigation.navigate('Profile'); + + return ( +
+ Header + + } + headerRight={ + <> + + + + + } + headerRightFadesIn + headerLeft={} + /> + ); +}; + +const LargeHeaderComponent: React.FC = ({ scrollY }) => { + const navigation = useNavigation(); + const onPressProfile = () => navigation.navigate('Profile'); + + return ( + + + Large Header + + + + + + ); +}; + +// Used for SectionList optimization +const ITEM_HEIGHT = 60; + +const SectionList: React.FC = () => { + const { bottom } = useSafeAreaInsets(); + + const renderItem: SectionListRenderItem = useCallback(({ item }) => { + return ( + + {item}. Scroll to see header animation + + ); + }, []); + + return ( + ( + {title} + )} + windowSize={10} + getItemLayout={(_, index) => ({ index, length: ITEM_HEIGHT, offset: index * ITEM_HEIGHT })} + initialNumToRender={50} + maxToRenderPerBatch={100} + disableAutoFixScroll + keyExtractor={(_, i) => `text-row-${i}`} + /> + ); +}; + +export default SectionList; + +const styles = StyleSheet.create({ + navBarTitle: { fontSize: 16, fontWeight: 'bold' }, + title: { fontSize: 32, fontWeight: 'bold' }, + sectionTitle: { + fontSize: 18, + fontWeight: 'bold', + width: '100%', + backgroundColor: '#E6E6E6', + paddingHorizontal: 12, + paddingVertical: 4, + }, + leftHeader: { gap: 2 }, + item: { minHeight: ITEM_HEIGHT, padding: 16, justifyContent: 'center', alignItems: 'center' }, + itemText: { textAlign: 'center' }, +}); diff --git a/src/components/containers/SectionList.tsx b/src/components/containers/SectionList.tsx new file mode 100644 index 0000000..128e139 --- /dev/null +++ b/src/components/containers/SectionList.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { View, SectionList, StyleSheet, SectionListProps } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import Animated, { useAnimatedRef, useScrollViewOffset } from 'react-native-reanimated'; + +import FadingView from '../containers/FadingView'; +import { useScrollContainerLogic } from './useScrollContainerLogic'; +import type { SharedScrollContainerProps } from './types'; +import type { DefaultSectionT } from 'react-native'; + +type AnimatedSectionListType = React.ComponentProps< + React.ComponentClass>, any> +> & + SharedScrollContainerProps; + +const AnimatedSectionList = Animated.createAnimatedComponent(SectionList) as React.ComponentClass< + Animated.AnimateProps>, + any +>; + +const AnimatedSectionListWithHeaders = ({ + largeHeaderShown, + containerStyle, + LargeHeaderComponent, + largeHeaderContainerStyle, + HeaderComponent, + onLargeHeaderLayout, + onScrollBeginDrag, + onScrollEndDrag, + onMomentumScrollBegin, + onMomentumScrollEnd, + ignoreLeftSafeArea, + ignoreRightSafeArea, + disableAutoFixScroll, + ...rest +}: AnimatedSectionListType) => { + const insets = useSafeAreaInsets(); + const scrollRef = useAnimatedRef(); + // Need to use `any` here because useScrollViewOffset is not typed for Animated.SectionList + const scrollY = useScrollViewOffset(scrollRef as any); + + const { showNavBar, largeHeaderHeight, largeHeaderOpacity, debouncedFixScroll } = + useScrollContainerLogic({ + scrollRef, + scrollY, + largeHeaderShown, + disableAutoFixScroll, + largeHeaderExists: !!LargeHeaderComponent, + }); + + return ( + + {HeaderComponent({ showNavBar })} + { + debouncedFixScroll.cancel(); + if (onScrollBeginDrag) onScrollBeginDrag(e); + }} + onScrollEndDrag={(e) => { + debouncedFixScroll(); + if (onScrollEndDrag) onScrollEndDrag(e); + }} + onMomentumScrollBegin={(e) => { + debouncedFixScroll.cancel(); + if (onMomentumScrollBegin) onMomentumScrollBegin(e); + }} + onMomentumScrollEnd={(e) => { + debouncedFixScroll(); + if (onMomentumScrollEnd) onMomentumScrollEnd(e); + }} + ListHeaderComponent={ + LargeHeaderComponent ? ( + { + largeHeaderHeight.value = e.nativeEvent.layout.height; + + if (onLargeHeaderLayout) onLargeHeaderLayout(e.nativeEvent.layout); + }} + > + + {LargeHeaderComponent({ scrollY, showNavBar })} + + + ) : undefined + } + {...rest} + /> + + ); +}; + +export default AnimatedSectionListWithHeaders; + +const styles = StyleSheet.create({ container: { flex: 1 } }); diff --git a/src/components/containers/index.ts b/src/components/containers/index.ts index d972358..424d4d2 100644 --- a/src/components/containers/index.ts +++ b/src/components/containers/index.ts @@ -2,6 +2,7 @@ export { default as ScalingView } from './ScalingView'; export { default as FadingView } from './FadingView'; export { default as ScrollViewWithHeaders } from './ScrollView'; export { default as FlatListWithHeaders } from './FlatList'; +export { default as SectionListWithHeaders } from './SectionList'; export type { ScrollHeaderProps, ScrollLargeHeaderProps, diff --git a/src/components/index.ts b/src/components/index.ts index c6acfa2..11fdc44 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,10 @@ -export { ScrollViewWithHeaders, FlatListWithHeaders, ScalingView, FadingView } from './containers'; +export { + FadingView, + ScalingView, + ScrollViewWithHeaders, + FlatListWithHeaders, + SectionListWithHeaders, +} from './containers'; export type { ScrollHeaderProps, ScrollLargeHeaderProps,