From 426e936740fcaba40d796a3f0aa12d888e88ac26 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 8 Nov 2025 14:14:33 +0530 Subject: [PATCH] parallax added --- src/components/home/AppleTVHero.tsx | 78 ++++++++++++++++++++++++++--- src/hooks/useSettings.ts | 2 +- src/screens/HomeScreen.tsx | 27 +++++++--- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/components/home/AppleTVHero.tsx b/src/components/home/AppleTVHero.tsx index 5121c1a1..b74f6396 100644 --- a/src/components/home/AppleTVHero.tsx +++ b/src/components/home/AppleTVHero.tsx @@ -27,6 +27,8 @@ import Animated, { runOnJS, interpolate, Extrapolation, + useAnimatedScrollHandler, + SharedValue, } from 'react-native-reanimated'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { StreamingContent } from '../../services/catalogService'; @@ -43,6 +45,7 @@ interface AppleTVHeroProps { allFeaturedContent?: StreamingContent[]; loading?: boolean; onRetry?: () => void; + scrollY?: SharedValue; // Optional scroll position for parallax } const { width, height } = Dimensions.get('window'); @@ -50,8 +53,8 @@ const { width, height } = Dimensions.get('window'); // Get status bar height const STATUS_BAR_HEIGHT = StatusBar.currentHeight || 0; -// Calculate hero height - 65% of screen height -const HERO_HEIGHT = height * 0.75; +// Calculate hero height - 85% of screen height +const HERO_HEIGHT = height * 0.85; // Animated Pagination Dot Component const PaginationDot: React.FC<{ isActive: boolean; onPress: () => void }> = React.memo( @@ -86,6 +89,7 @@ const AppleTVHero: React.FC = ({ allFeaturedContent, loading, onRetry, + scrollY: externalScrollY, }) => { const navigation = useNavigation>(); const isFocused = useIsFocused(); @@ -94,6 +98,10 @@ const AppleTVHero: React.FC = ({ const { settings, updateSetting } = useSettings(); const { isTrailerPlaying: globalTrailerPlaying, setTrailerPlaying } = useTrailer(); + // Create internal scrollY if not provided externally + const internalScrollY = useSharedValue(0); + const scrollY = externalScrollY || internalScrollY; + // Determine items to display const items = useMemo(() => { if (allFeaturedContent && allFeaturedContent.length > 0) { @@ -160,6 +168,62 @@ const AppleTVHero: React.FC = ({ }; }); + // Parallax style for background images + const backgroundParallaxStyle = useAnimatedStyle(() => { + 'worklet'; + const scrollYValue = scrollY.value; + + // Pre-calculated constants - start at 1.0 for normal size + const DEFAULT_ZOOM = 1.0; + const SCROLL_UP_MULTIPLIER = 0.002; + const SCROLL_DOWN_MULTIPLIER = 0.0001; + const MAX_SCALE = 1.3; + const PARALLAX_FACTOR = 0.3; + + // Optimized scale calculation with minimal branching + const scrollUpScale = DEFAULT_ZOOM + Math.abs(scrollYValue) * SCROLL_UP_MULTIPLIER; + const scrollDownScale = DEFAULT_ZOOM + scrollYValue * SCROLL_DOWN_MULTIPLIER; + const scale = Math.min(scrollYValue < 0 ? scrollUpScale : scrollDownScale, MAX_SCALE); + + // Single parallax calculation + const parallaxOffset = scrollYValue * PARALLAX_FACTOR; + + return { + transform: [ + { scale }, + { translateY: parallaxOffset } + ], + }; + }); + + // Parallax style for trailer + const trailerParallaxStyle = useAnimatedStyle(() => { + 'worklet'; + const scrollYValue = scrollY.value; + + // Pre-calculated constants - start at 1.0 for normal size + const DEFAULT_ZOOM = 1.0; + const SCROLL_UP_MULTIPLIER = 0.0015; + const SCROLL_DOWN_MULTIPLIER = 0.0001; + const MAX_SCALE = 1.2; + const PARALLAX_FACTOR = 0.2; // Slower than background for depth + + // Optimized scale calculation with minimal branching + const scrollUpScale = DEFAULT_ZOOM + Math.abs(scrollYValue) * SCROLL_UP_MULTIPLIER; + const scrollDownScale = DEFAULT_ZOOM + scrollYValue * SCROLL_DOWN_MULTIPLIER; + const scale = Math.min(scrollYValue < 0 ? scrollUpScale : scrollDownScale, MAX_SCALE); + + // Single parallax calculation + const parallaxOffset = scrollYValue * PARALLAX_FACTOR; + + return { + transform: [ + { scale }, + { translateY: parallaxOffset } + ], + }; + }); + // Reset loaded states when items change useEffect(() => { setBannerLoaded({}); @@ -585,7 +649,7 @@ const AppleTVHero: React.FC = ({ {/* Background Images with Crossfade */} {/* Current Image - Always visible as base */} - + = ({ resizeMode={FastImage.resizeMode.cover} onLoad={() => setBannerLoaded((prev) => ({ ...prev, [currentIndex]: true }))} /> - + {/* Next/Preview Image - Animated overlay during drag */} {nextIndex !== currentIndex && ( - + = ({ {/* Visible trailer player - 60% height with 5% zoom and smooth fade */} {settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && ( - + = ({ {settings?.showTrailers && trailerReady && trailerUrl && ( { const refreshTimeoutRef = useRef(null); const [hasContinueWatching, setHasContinueWatching] = useState(false); + // Shared value for scroll position (for parallax effects) + const scrollY = useSharedValue(0); + const [catalogs, setCatalogs] = useState<(CatalogContent | null)[]>([]); const [catalogsLoading, setCatalogsLoading] = useState(true); const [loadedCatalogCount, setLoadedCatalogCount] = useState(0); @@ -642,6 +645,7 @@ const HomeScreen = () => { featuredContent={featuredContent || null} allFeaturedContent={allFeaturedContent || []} loading={featuredLoading} + scrollY={scrollY} /> ); } else if (heroStyleToUse === 'carousel') { @@ -786,11 +790,14 @@ const HomeScreen = () => { } // Capture scroll values immediately before async operation - const scrollY = event.nativeEvent.contentOffset.y; + const scrollYValue = event.nativeEvent.contentOffset.y; + + // Update shared value for parallax (on UI thread) + scrollY.value = scrollYValue; // Use requestAnimationFrame to throttle scroll handling scrollAnimationFrameRef.current = requestAnimationFrame(() => { - const y = scrollY; + const y = scrollYValue; const dy = y - lastScrollYRef.current; lastScrollYRef.current = y; @@ -812,10 +819,16 @@ const HomeScreen = () => { }, [toggleHeader]); // Memoize content container style - use stable insets to prevent iOS shifting - const contentContainerStyle = useMemo(() => - StyleSheet.flatten([styles.scrollContent, { paddingTop: stableInsetsTop }]), - [stableInsetsTop] - ); + // Don't add paddingTop when using AppleTVHero as it handles its own top spacing + const contentContainerStyle = useMemo(() => { + const heroStyleToUse = settings.heroStyle; + const isUsingAppleTVHero = heroStyleToUse === 'appletv' && !isTablet && showHeroSection; + + return StyleSheet.flatten([ + styles.scrollContent, + { paddingTop: isUsingAppleTVHero ? 0 : stableInsetsTop } + ]); + }, [stableInsetsTop, settings.heroStyle, isTablet, showHeroSection]); // Memoize the main content section const renderMainContent = useMemo(() => {