From 9c73af1d47986bac8b2efe200fe372af28da1724 Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 9 Jun 2025 13:25:50 +0530 Subject: [PATCH] Refactor FloatingHeader component for improved performance and readability. Introduce memoization for padding and header title calculations, optimizing rendering. Remove unused props and streamline animated styles for better efficiency. Update styles for consistency and enhance user interaction with improved hitSlop settings. --- src/components/metadata/HeroSection.tsx | 232 +++++++++++++----------- src/navigation/AppNavigator.tsx | 34 +++- 2 files changed, 157 insertions(+), 109 deletions(-) diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index 6734ac3..9c17a75 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -15,6 +15,7 @@ import Animated, { Extrapolate, useSharedValue, withTiming, + runOnJS, } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; import { logger } from '../../utils/logger'; @@ -22,7 +23,12 @@ import { TMDBService } from '../../services/tmdbService'; const { width, height } = Dimensions.get('window'); -// Types - optimized +// Ultra-optimized animation constants +const PARALLAX_FACTOR = 0.3; +const SCALE_FACTOR = 1.02; +const FADE_THRESHOLD = 200; + +// Types - streamlined interface HeroSectionProps { metadata: any; bannerImage: string | null; @@ -54,7 +60,7 @@ interface HeroSectionProps { setLogoLoadError: (error: boolean) => void; } -// Ultra-optimized ActionButtons Component with minimal re-renders +// Ultra-optimized ActionButtons Component - minimal re-renders const ActionButtons = React.memo(({ handleShowStreams, toggleLibrary, @@ -76,7 +82,7 @@ const ActionButtons = React.memo(({ }) => { const { currentTheme } = useTheme(); - // Memoized navigation handler for better performance + // Memoized navigation handler const handleRatingsPress = useMemo(() => async () => { let finalTmdbId: number | null = null; @@ -92,7 +98,6 @@ const ActionButtons = React.memo(({ const convertedId = await tmdbService.findTMDBIdByIMDB(id); if (convertedId) { finalTmdbId = convertedId; - logger.log(`[HeroSection] Converted IMDb ID ${id} to TMDB ID: ${finalTmdbId}`); } } catch (error) { logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error); @@ -114,7 +119,7 @@ const ActionButtons = React.memo(({ = ({ }) => { const { currentTheme } = useTheme(); - // Optimized state management + // Minimal state for image handling const [imageError, setImageError] = useState(false); const imageOpacity = useSharedValue(1); - // Memoized image source for better performance + // Memoized image source const imageSource = useMemo(() => bannerImage || metadata.banner || metadata.poster , [bannerImage, metadata.banner, metadata.poster]); - // Optimized image handlers + // Ultra-fast image handlers const handleImageError = () => { - logger.warn(`[HeroSection] Banner failed to load: ${imageSource}`); setImageError(true); - imageOpacity.value = withTiming(0.7, { duration: 150 }); - if (bannerImage !== metadata.banner) { - setBannerImage(metadata.banner || metadata.poster); - } + imageOpacity.value = withTiming(0.6, { duration: 150 }); + runOnJS(() => { + if (bannerImage !== metadata.banner) { + setBannerImage(metadata.banner || metadata.poster); + } + })(); }; const handleImageLoad = () => { setImageError(false); - imageOpacity.value = withTiming(1, { duration: 200 }); + imageOpacity.value = withTiming(1, { duration: 150 }); }; - // Ultra-optimized animated styles with minimal calculations + // Ultra-optimized animated styles - single calculations const heroAnimatedStyle = useAnimatedStyle(() => ({ height: heroHeight.value, opacity: heroOpacity.value, @@ -273,56 +279,60 @@ const HeroSection: React.FC = ({ const logoAnimatedStyle = useAnimatedStyle(() => ({ opacity: logoOpacity.value, + transform: [{ + translateY: interpolate( + scrollY.value, + [0, 100], + [0, -20], + Extrapolate.CLAMP + ) + }] }), []); const watchProgressAnimatedStyle = useAnimatedStyle(() => ({ opacity: watchProgressOpacity.value, }), []); - // Simplified backdrop animation - fewer calculations - const backdropImageStyle = useAnimatedStyle(() => ({ - opacity: imageOpacity.value, - transform: [ - { - translateY: interpolate( - scrollY.value, - [0, 200], - [0, -60], - Extrapolate.CLAMP - ) - }, - { - scale: interpolate( - scrollY.value, - [0, 200], - [1.05, 1.02], - Extrapolate.CLAMP - ) - }, - ], - }), []); + // Ultra-optimized backdrop with minimal calculations + const backdropImageStyle = useAnimatedStyle(() => { + 'worklet'; + const translateY = scrollY.value * PARALLAX_FACTOR; + const scale = 1 + (scrollY.value * 0.0001); // Micro scale effect + + return { + opacity: imageOpacity.value, + transform: [ + { translateY: -Math.min(translateY, 100) }, // Cap translation + { scale: Math.min(scale, SCALE_FACTOR) } // Cap scale + ], + }; + }, []); + // Simplified buttons animation const buttonsAnimatedStyle = useAnimatedStyle(() => ({ opacity: buttonsOpacity.value, - transform: [{ translateY: buttonsTranslateY.value }] + transform: [{ + translateY: interpolate( + buttonsTranslateY.value, + [0, 20], + [0, 20], + Extrapolate.CLAMP + ) + }] }), []); - // Memoized genre rendering for performance + // Ultra-optimized genre rendering const genreElements = useMemo(() => { - if (!metadata?.genres || !Array.isArray(metadata.genres) || metadata.genres.length === 0) { - return null; - } + if (!metadata?.genres?.length) return null; - const genresToDisplay: string[] = metadata.genres.slice(0, 4); + const genresToDisplay = metadata.genres.slice(0, 3); // Reduced to 3 for performance return genresToDisplay.map((genreName: string, index: number, array: string[]) => ( - + {genreName} {index < array.length - 1 && ( - - • - + )} )); @@ -333,11 +343,11 @@ const HeroSection: React.FC = ({ return ( - {/* Background Layer */} + {/* Optimized Background */} - {/* Background Image - Optimized */} - {!loadingBanner && imageSource && ( + {/* Ultra-optimized Background Image */} + {imageSource && !loadingBanner && ( = ({ /> )} - {/* Gradient Overlay */} + {/* Simplified Gradient */} - {/* Title/Logo */} + {/* Optimized Title/Logo */} {metadata.logo && !logoLoadError ? ( @@ -368,10 +377,9 @@ const HeroSection: React.FC = ({ source={{ uri: metadata.logo }} style={styles.titleLogo} contentFit="contain" - transition={200} + transition={150} onError={() => { - logger.warn(`[HeroSection] Logo failed to load: ${metadata.logo}`); - setLogoLoadError(true); + runOnJS(setLogoLoadError)(true); }} /> ) : ( @@ -382,7 +390,7 @@ const HeroSection: React.FC = ({ - {/* Watch Progress */} + {/* Optimized Watch Progress */} = ({ animatedStyle={watchProgressAnimatedStyle} /> - {/* Genres */} + {/* Optimized Genres */} {genreElements && ( {genreElements} )} - {/* Action Buttons */} + {/* Optimized Action Buttons */} = ({ ); }; -// Optimized styles with minimal properties +// Ultra-optimized styles const styles = StyleSheet.create({ heroSection: { width: '100%', @@ -431,17 +439,18 @@ const styles = StyleSheet.create({ heroGradient: { flex: 1, justifyContent: 'flex-end', - paddingBottom: 24, + paddingBottom: 20, }, heroContent: { padding: 16, - paddingTop: 12, - paddingBottom: 12, + paddingTop: 8, + paddingBottom: 8, }, logoContainer: { alignItems: 'center', justifyContent: 'center', width: '100%', + marginBottom: 4, }, titleLogoContainer: { alignItems: 'center', @@ -449,18 +458,18 @@ const styles = StyleSheet.create({ width: '100%', }, titleLogo: { - width: width * 0.8, - height: 100, + width: width * 0.75, + height: 90, alignSelf: 'center', }, heroTitle: { - fontSize: 28, + fontSize: 26, fontWeight: '900', - marginBottom: 12, - textShadowColor: 'rgba(0,0,0,0.75)', - textShadowOffset: { width: 0, height: 2 }, - textShadowRadius: 4, - letterSpacing: -0.5, + marginBottom: 8, + textShadowColor: 'rgba(0,0,0,0.8)', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 3, + letterSpacing: -0.3, textAlign: 'center', }, genreContainer: { @@ -468,18 +477,20 @@ const styles = StyleSheet.create({ flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', - marginTop: 8, - marginBottom: 16, - gap: 4, + marginTop: 6, + marginBottom: 14, + gap: 6, }, genreText: { fontSize: 12, fontWeight: '500', + opacity: 0.9, }, genreDot: { fontSize: 12, fontWeight: '500', - marginHorizontal: 4, + opacity: 0.6, + marginHorizontal: 2, }, actionButtons: { flexDirection: 'row', @@ -492,65 +503,70 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - paddingVertical: 12, + paddingVertical: 11, paddingHorizontal: 16, - borderRadius: 28, + borderRadius: 26, flex: 1, }, playButton: { backgroundColor: '#fff', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 4, }, infoButton: { - backgroundColor: 'rgba(255,255,255,0.2)', - borderWidth: 2, - borderColor: '#fff', + backgroundColor: 'rgba(255,255,255,0.15)', + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.7)', }, iconButton: { - width: 52, - height: 52, - borderRadius: 26, - backgroundColor: 'rgba(255,255,255,0.2)', - borderWidth: 2, - borderColor: '#fff', + width: 50, + height: 50, + borderRadius: 25, + backgroundColor: 'rgba(255,255,255,0.15)', + borderWidth: 1.5, + borderColor: 'rgba(255,255,255,0.7)', alignItems: 'center', justifyContent: 'center', }, playButtonText: { color: '#000', - fontWeight: '600', - marginLeft: 8, - fontSize: 16, + fontWeight: '700', + marginLeft: 6, + fontSize: 15, }, infoButtonText: { color: '#fff', - marginLeft: 8, + marginLeft: 6, fontWeight: '600', - fontSize: 16, + fontSize: 15, }, watchProgressContainer: { - marginTop: 6, - marginBottom: 8, + marginTop: 4, + marginBottom: 6, width: '100%', alignItems: 'center', - height: 48, + height: 44, }, watchProgressBar: { - width: '75%', - height: 3, - backgroundColor: 'rgba(255, 255, 255, 0.15)', - borderRadius: 1.5, + width: '70%', + height: 2.5, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 1.25, overflow: 'hidden', marginBottom: 6 }, watchProgressFill: { height: '100%', - borderRadius: 1.5, + borderRadius: 1.25, }, watchProgressText: { - fontSize: 12, + fontSize: 11, textAlign: 'center', - opacity: 0.9, - letterSpacing: 0.2 + opacity: 0.85, + letterSpacing: 0.1 }, }); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 2a5b2c1..c5498d0 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -658,6 +658,32 @@ const MainTabs = () => { ); }; +// Create custom fade animation interpolator for MetadataScreen +const customFadeInterpolator = ({ current, layouts }: any) => { + return { + cardStyle: { + opacity: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 1], + }), + transform: [ + { + scale: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [0.95, 1], + }), + }, + ], + }, + overlayStyle: { + opacity: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [0, 0.3], + }), + }, + }; +}; + // Stack Navigator const AppNavigator = () => { const { currentTheme } = useTheme(); @@ -731,8 +757,14 @@ const AppNavigator = () => { component={MetadataScreen} options={{ headerShown: false, - animation: 'slide_from_right', + animation: Platform.OS === 'ios' ? 'fade' : 'slide_from_right', animationDuration: Platform.OS === 'android' ? 250 : 300, + ...(Platform.OS === 'ios' && { + cardStyleInterpolator: customFadeInterpolator, + animationTypeForReplace: 'push', + gestureEnabled: true, + gestureDirection: 'horizontal', + }), contentStyle: { backgroundColor: currentTheme.colors.darkBackground, },