From aca214c80a18c2a55640f2b8c71dd053bd4eb98d Mon Sep 17 00:00:00 2001 From: eyounan Date: Wed, 28 Jun 2023 21:14:30 -0400 Subject: [PATCH] feat: implemented absolute header with blur example --- example/src/navigation/AppNavigation.tsx | 5 + example/src/navigation/types.ts | 6 + example/src/screens/Home.tsx | 6 + example/src/screens/index.ts | 1 + .../usage/AbsoluteHeaderBlurSurface.tsx | 105 ++++++++++++++++++ src/components/containers/FlashList.tsx | 39 ++++++- src/components/containers/FlatList.tsx | 38 ++++++- src/components/containers/ScrollView.tsx | 36 +++++- src/components/containers/SectionList.tsx | 38 ++++++- src/components/containers/types.ts | 11 ++ .../containers/useScrollContainerLogic.ts | 27 +++++ 11 files changed, 304 insertions(+), 8 deletions(-) create mode 100644 example/src/screens/usage/AbsoluteHeaderBlurSurface.tsx diff --git a/example/src/navigation/AppNavigation.tsx b/example/src/navigation/AppNavigation.tsx index c7817cb..8cf99a9 100644 --- a/example/src/navigation/AppNavigation.tsx +++ b/example/src/navigation/AppNavigation.tsx @@ -10,6 +10,7 @@ import { SimpleUsageScreen, SurfaceComponentUsageScreen, TwitterProfileScreen, + AbsoluteHeaderBlurSurface, } from '../screens'; const Stack = createNativeStackNavigator(); @@ -31,5 +32,9 @@ export default () => ( component={SurfaceComponentUsageScreen} /> + ); diff --git a/example/src/navigation/types.ts b/example/src/navigation/types.ts index fe25e69..a43051a 100644 --- a/example/src/navigation/types.ts +++ b/example/src/navigation/types.ts @@ -10,6 +10,7 @@ export type RootStackParamList = { SectionListUsageScreen: undefined; TwitterProfileScreen: undefined; HeaderSurfaceComponentUsageScreen: undefined; + AbsoluteHeaderBlurSurfaceUsageScreen: undefined; }; // Overrides the typing for useNavigation in @react-navigation/native to support the internal @@ -53,3 +54,8 @@ export type TwitterProfileScreenNavigationProps = NativeStackScreenProps< RootStackParamList, 'TwitterProfileScreen' >; + +export type AbsoluteHeaderBlurSurfaceUsageScreenNavigationProps = NativeStackScreenProps< + RootStackParamList, + 'AbsoluteHeaderBlurSurfaceUsageScreen' +>; diff --git a/example/src/screens/Home.tsx b/example/src/screens/Home.tsx index b6ca126..3bdb122 100644 --- a/example/src/screens/Home.tsx +++ b/example/src/screens/Home.tsx @@ -49,6 +49,11 @@ const SCREEN_LIST_CONFIG: ScreenConfigItem[] = [ route: 'TwitterProfileScreen', description: 'Rebuilding the Twitter profile header with this library.', }, + { + name: 'Absolute Header with Blurred Surface', + route: 'AbsoluteHeaderBlurSurfaceUsageScreen', + description: 'An example of an absolutely-positioned header with a BlurView surface.', + }, ]; const HeaderComponent: React.FC = ({ showNavBar }) => { @@ -83,6 +88,7 @@ const Home: React.FC = ({ navigation }) => { return ( = ({ showNavBar }) => { + return ( + + + + ); +}; + +const HeaderComponent: React.FC = ({ showNavBar }) => { + const insets = useSafeAreaInsets(); + + return ( +
react-native-header} + headerLeft={} + SurfaceComponent={HeaderSurface} + /> + ); +}; + +const LargeHeaderComponent: React.FC = ({ scrollY }) => { + return ( + + + Large Header + + + ); +}; + +const TransparentSurface: React.FC = () => { + const { bottom } = useSafeAreaInsets(); + + return ( + + + {range({ end: 20 }).map((i) => ( + + ))} + + + ); +}; + +export default TransparentSurface; + +const styles = StyleSheet.create({ + container: { + flex: 1, + zIndex: -100, + }, + contentContainer: { + paddingTop: 44, + }, + redBox: { + backgroundColor: 'red', + height: 200, + width: 200, + }, + greenBox: { + backgroundColor: 'green', + height: 200, + width: 200, + }, + headerTitle: { + fontSize: 16, + fontWeight: 'bold', + }, + boxes: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: 12, + flexWrap: 'wrap', + }, + title: { fontSize: 32, fontWeight: 'bold' }, + leftHeader: { gap: 2 }, +}); diff --git a/src/components/containers/FlashList.tsx b/src/components/containers/FlashList.tsx index 5844a52..7c6f9ae 100644 --- a/src/components/containers/FlashList.tsx +++ b/src/components/containers/FlashList.tsx @@ -35,6 +35,10 @@ const FlashListWithHeadersInputComp = ( disableAutoFixScroll = false, /** At the moment, we will not allow overriding of this since the scrollHandler needs it. */ onScroll: _unusedOnScroll, + absoluteHeader = false, + initialAbsoluteHeaderHeight = 0, + contentContainerStyle = {}, + automaticallyAdjustsScrollIndicatorInsets, ...rest }: AnimatedFlashListType, ref: React.Ref> @@ -50,11 +54,15 @@ const FlashListWithHeadersInputComp = ( largeHeaderOpacity, scrollHandler, debouncedFixScroll, + absoluteHeaderHeight, + onAbsoluteHeaderLayout, } = useScrollContainerLogic({ scrollRef, largeHeaderShown, disableAutoFixScroll, largeHeaderExists: !!LargeHeaderComponent, + absoluteHeader, + initialAbsoluteHeaderHeight, }); return ( @@ -66,7 +74,7 @@ const FlashListWithHeadersInputComp = ( !ignoreRightSafeArea && { paddingRight: insets.right }, ]} > - {HeaderComponent({ showNavBar, scrollY })} + {!absoluteHeader && HeaderComponent({ showNavBar, scrollY })} ( debouncedFixScroll(); if (onMomentumScrollEnd) onMomentumScrollEnd(e); }} + // eslint-disable-next-line react-native/no-inline-styles + contentContainerStyle={{ + // The reason why we do this is because FlashList does not support an array of + // styles (will throw a warning when you supply one). + ...contentContainerStyle, + paddingTop: absoluteHeader ? absoluteHeaderHeight : 0, + }} + automaticallyAdjustsScrollIndicatorInsets={ + automaticallyAdjustsScrollIndicatorInsets !== undefined + ? automaticallyAdjustsScrollIndicatorInsets + : !absoluteHeader + } + scrollIndicatorInsets={{ top: absoluteHeader ? absoluteHeaderHeight : 0 }} ListHeaderComponent={ LargeHeaderComponent ? ( ( } {...rest} /> + + {absoluteHeader && ( + + {HeaderComponent({ showNavBar, scrollY })} + + )} ); }; @@ -117,4 +144,12 @@ const FlashListWithHeaders = React.forwardRef(FlashListWithHeadersInputComp) as export default FlashListWithHeaders; -const styles = StyleSheet.create({ container: { flex: 1 } }); +const styles = StyleSheet.create({ + container: { flex: 1 }, + absoluteHeader: { + position: 'absolute', + top: 0, + right: 0, + left: 0, + }, +}); diff --git a/src/components/containers/FlatList.tsx b/src/components/containers/FlatList.tsx index 43944ae..d7ca0b7 100644 --- a/src/components/containers/FlatList.tsx +++ b/src/components/containers/FlatList.tsx @@ -31,6 +31,10 @@ const FlatListWithHeadersInputComp = ( disableAutoFixScroll = false, /** At the moment, we will not allow overriding of this since the scrollHandler needs it. */ onScroll: _unusedOnScroll, + absoluteHeader = false, + initialAbsoluteHeaderHeight = 0, + contentContainerStyle, + automaticallyAdjustsScrollIndicatorInsets, ...rest }: AnimatedFlatListProps & SharedScrollContainerProps, ref: React.Ref | null> @@ -46,11 +50,15 @@ const FlatListWithHeadersInputComp = ( largeHeaderOpacity, scrollHandler, debouncedFixScroll, + absoluteHeaderHeight, + onAbsoluteHeaderLayout, } = useScrollContainerLogic({ scrollRef, largeHeaderShown, disableAutoFixScroll, largeHeaderExists: !!LargeHeaderComponent, + absoluteHeader, + initialAbsoluteHeaderHeight, }); return ( @@ -62,7 +70,7 @@ const FlatListWithHeadersInputComp = ( !ignoreRightSafeArea && { paddingRight: insets.right }, ]} > - {HeaderComponent({ showNavBar, scrollY })} + {!absoluteHeader && HeaderComponent({ showNavBar, scrollY })} ( debouncedFixScroll(); if (onMomentumScrollEnd) onMomentumScrollEnd(e); }} + contentContainerStyle={[ + // @ts-ignore + // Unfortunately there are issues with Reanimated typings, so will ignore for now. + contentContainerStyle, + absoluteHeader ? { paddingTop: absoluteHeaderHeight } : undefined, + ]} + automaticallyAdjustsScrollIndicatorInsets={ + automaticallyAdjustsScrollIndicatorInsets !== undefined + ? automaticallyAdjustsScrollIndicatorInsets + : !absoluteHeader + } + scrollIndicatorInsets={{ top: absoluteHeader ? absoluteHeaderHeight : 0 }} ListHeaderComponent={ LargeHeaderComponent ? ( ( } {...rest} /> + + {absoluteHeader && ( + + {HeaderComponent({ showNavBar, scrollY })} + + )} ); }; @@ -114,4 +140,12 @@ const FlatListWithHeaders = React.forwardRef(FlatListWithHeadersInputComp) as @@ -46,11 +50,15 @@ const ScrollViewWithHeadersInputComp = ( largeHeaderOpacity, scrollHandler, debouncedFixScroll, + absoluteHeaderHeight, + onAbsoluteHeaderLayout, } = useScrollContainerLogic({ scrollRef, largeHeaderShown, disableAutoFixScroll, largeHeaderExists: !!LargeHeaderComponent, + absoluteHeader, + initialAbsoluteHeaderHeight, }); return ( @@ -62,7 +70,7 @@ const ScrollViewWithHeadersInputComp = ( !ignoreRightSafeArea && { paddingRight: insets.right }, ]} > - {HeaderComponent({ showNavBar, scrollY })} + {!absoluteHeader && HeaderComponent({ showNavBar, scrollY })} {LargeHeaderComponent ? ( @@ -102,6 +120,12 @@ const ScrollViewWithHeadersInputComp = ( ) : null} {children} + + {absoluteHeader && ( + + {HeaderComponent({ showNavBar, scrollY })} + + )} ); }; @@ -113,4 +137,12 @@ const ScrollViewWithHeaders = React.forwardRef< export default ScrollViewWithHeaders; -const styles = StyleSheet.create({ container: { flex: 1 } }); +const styles = StyleSheet.create({ + container: { flex: 1 }, + absoluteHeader: { + position: 'absolute', + top: 0, + right: 0, + left: 0, + }, +}); diff --git a/src/components/containers/SectionList.tsx b/src/components/containers/SectionList.tsx index 7606b36..b5caed5 100644 --- a/src/components/containers/SectionList.tsx +++ b/src/components/containers/SectionList.tsx @@ -35,6 +35,10 @@ const SectionListWithHeadersInputComp = , ref: React.Ref @@ -50,11 +54,15 @@ const SectionListWithHeadersInputComp = - {HeaderComponent({ showNavBar, scrollY })} + {!absoluteHeader && HeaderComponent({ showNavBar, scrollY })} + + {absoluteHeader && ( + + {HeaderComponent({ showNavBar, scrollY })} + + )} ); }; @@ -120,4 +146,12 @@ const SectionListWithHeaders = React.forwardRef(SectionListWithHeadersInputComp) export default SectionListWithHeaders; -const styles = StyleSheet.create({ container: { flex: 1 } }); +const styles = StyleSheet.create({ + container: { flex: 1 }, + absoluteHeader: { + position: 'absolute', + top: 0, + right: 0, + left: 0, + }, +}); diff --git a/src/components/containers/types.ts b/src/components/containers/types.ts index b7f2688..0e57569 100644 --- a/src/components/containers/types.ts +++ b/src/components/containers/types.ts @@ -124,4 +124,15 @@ export type SharedScrollContainerProps = { * scroll events, use the useScrollViewOffset hook with a ref. */ onScroll?: React.ComponentProps['onScroll']; + /** + * This property controls whether or not the header component is absolutely positioned. + * This is useful if you want to render a header component that allows for transparency. + */ + absoluteHeader?: boolean; + /** + * This property is used when `absoluteHeader` is true. This is the initial height of the + * absolute header. Since the header's height is computed on its layout event, this is used + * to set the initial height of the header so that it doesn't jump when it is initially rendered. + */ + initialAbsoluteHeaderHeight?: number; }; diff --git a/src/components/containers/useScrollContainerLogic.ts b/src/components/containers/useScrollContainerLogic.ts index 06f6034..054afdf 100644 --- a/src/components/containers/useScrollContainerLogic.ts +++ b/src/components/containers/useScrollContainerLogic.ts @@ -1,3 +1,5 @@ +import { useCallback, useState } from 'react'; +import { LayoutChangeEvent } from 'react-native'; import Animated, { interpolate, runOnUI, @@ -45,6 +47,17 @@ interface UseScrollContainerLogicArgs { * @default false */ disableAutoFixScroll?: boolean; + /** + * This property controls whether or not the header component is absolutely positioned. + * This is useful if you want to render a header component that allows for transparency. + */ + absoluteHeader?: boolean; + /** + * This property is used when `absoluteHeader` is true. This is the initial height of the + * absolute header. Since the header's height is computed on its layout event, this is used + * to set the initial height of the header so that it doesn't jump when it is initially rendered. + */ + initialAbsoluteHeaderHeight?: number; } /** @@ -59,7 +72,10 @@ export const useScrollContainerLogic = ({ largeHeaderExists, disableAutoFixScroll = false, adjustmentOffset = 4, + absoluteHeader = false, + initialAbsoluteHeaderHeight = 0, }: UseScrollContainerLogicArgs) => { + const [absoluteHeaderHeight, setAbsoluteHeaderHeight] = useState(initialAbsoluteHeaderHeight); const scrollY = useSharedValue(0); const largeHeaderHeight = useSharedValue(0); @@ -110,6 +126,15 @@ export const useScrollContainerLogic = ({ } }, 50); + const onAbsoluteHeaderLayout = useCallback( + (e: LayoutChangeEvent) => { + if (absoluteHeader) { + setAbsoluteHeaderHeight(e.nativeEvent.layout.height); + } + }, + [absoluteHeader] + ); + return { scrollY, showNavBar, @@ -117,5 +142,7 @@ export const useScrollContainerLogic = ({ largeHeaderOpacity, scrollHandler, debouncedFixScroll, + absoluteHeaderHeight, + onAbsoluteHeaderLayout, }; };