From c852c56231f9049f8dd6f3598cb7ff602a78bf07 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 21 Oct 2025 17:49:49 +0530 Subject: [PATCH] orientation optimization --- src/components/home/HeroCarousel.tsx | 275 +++++++++------------------ src/screens/HomeScreen.tsx | 16 +- 2 files changed, 104 insertions(+), 187 deletions(-) diff --git a/src/components/home/HeroCarousel.tsx b/src/components/home/HeroCarousel.tsx index b510875..45aeb01 100644 --- a/src/components/home/HeroCarousel.tsx +++ b/src/components/home/HeroCarousel.tsx @@ -19,7 +19,6 @@ if (Platform.OS === 'ios') { liquidGlassAvailable = false; } } -import { MaterialIcons } from '@expo/vector-icons'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; @@ -44,7 +43,7 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = const insets = useSafeAreaInsets(); const { settings } = useSettings(); - const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]); + const data = useMemo(() => (items && items.length ? items.slice(0, 5) : []), [items]); const [activeIndex, setActiveIndex] = useState(0); const [failedLogoIds, setFailedLogoIds] = useState>(new Set()); const scrollViewRef = useRef(null); @@ -102,7 +101,8 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = }, }); - // Derive the index reactively and only set state when it changes + // Debounced activeIndex update to reduce JS bridge crossings + const lastIndexUpdateRef = useRef(0); useAnimatedReaction( () => { const idx = Math.round(scrollX.value / interval); @@ -110,6 +110,12 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = }, (idx, prevIdx) => { if (idx == null || idx === prevIdx) return; + + // Debounce updates to reduce JS bridge crossings + const now = Date.now(); + if (now - lastIndexUpdateRef.current < 100) return; // 100ms debounce + lastIndexUpdateRef.current = now; + // Clamp to bounds to avoid out-of-range access const clamped = Math.max(0, Math.min(idx, data.length - 1)); runOnJS(setActiveIndex)(clamped); @@ -123,23 +129,20 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = navigation.navigate('Metadata', { id, type }); }, [navigation]); - const handleNavigateToStreams = useCallback((id: string, type: any) => { - navigation.navigate('Streams', { id, type }); - }, [navigation]); - // Container animation based on scroll - must be before early returns - const containerAnimatedStyle = useAnimatedStyle(() => { - const translateX = scrollX.value; - const progress = Math.abs(translateX) / (data.length * (CARD_WIDTH + 16)); - - // Very subtle scale animation for the entire container - const scale = 1 - progress * 0.01; - const clampedScale = Math.max(0.99, Math.min(1, scale)); - - return { - transform: [{ scale: clampedScale }], - }; - }); + // TEMPORARILY DISABLED FOR PERFORMANCE TESTING + // const containerAnimatedStyle = useAnimatedStyle(() => { + // const translateX = scrollX.value; + // const progress = Math.abs(translateX) / (data.length * (CARD_WIDTH + 16)); + // + // // Very subtle scale animation for the entire container + // const scale = 1 - progress * 0.01; + // const clampedScale = Math.max(0.99, Math.min(1, scale)); + // + // return { + // transform: [{ scale: clampedScale }], + // }; + // }); if (loading) { return ( @@ -193,18 +196,6 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = item: StreamingContent; insets: any; }) => { - const animatedOpacity = useSharedValue(1); - - useEffect(() => { - // Start with opacity 0 and animate to 1, but only if it's a new item - animatedOpacity.value = 0; - animatedOpacity.value = withTiming(1, { duration: 400 }); - }, [item.id]); - - const animatedStyle = useAnimatedStyle(() => ({ - opacity: animatedOpacity.value, - })); - return ( = ({ items, loading = false }) = ] as StyleProp} pointerEvents="none" > - {Platform.OS === 'android' ? ( = ({ items, loading = false }) = locations={[0.4, 1]} style={styles.backgroundOverlay as ViewStyle} /> - + ); }); @@ -263,33 +254,8 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = return ( - - {settings.enableHomeHeroBackground && data.length > 0 && ( - - {data[activeIndex + 1] && ( - - )} - {activeIndex > 0 && data[activeIndex - 1] && ( - - )} - - )} + + {/* Removed preload images for performance - let FastImage cache handle it naturally */} {settings.enableHomeHeroBackground && data[activeIndex] && ( = ({ items, loading = false }) = decelerationRate="fast" contentContainerStyle={contentPadding} onScroll={scrollHandler} - scrollEventThrottle={8} + scrollEventThrottle={32} disableIntervalMomentum pagingEnabled={false} bounces={false} @@ -327,7 +293,6 @@ const HeroCarousel: React.FC = ({ items, loading = false }) = logoFailed={failedLogoIds.has(item.id)} onLogoError={() => setFailedLogoIds((prev) => new Set(prev).add(item.id))} onPressInfo={() => handleNavigateToMetadata(item.id, item.type)} - onPressPlay={() => handleNavigateToStreams(item.id, item.type)} scrollX={scrollX} index={index} /> @@ -343,13 +308,12 @@ interface CarouselCardProps { colors: any; logoFailed: boolean; onLogoError: () => void; - onPressPlay: () => void; onPressInfo: () => void; scrollX: SharedValue; index: number; } -const CarouselCard: React.FC = memo(({ item, colors, logoFailed, onLogoError, onPressPlay, onPressInfo, scrollX, index }) => { +const CarouselCard: React.FC = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index }) => { const [bannerLoaded, setBannerLoaded] = useState(false); const [logoLoaded, setLogoLoaded] = useState(false); @@ -383,31 +347,28 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail opacity: logoOpacity.value, })); - const genresAnimatedStyle = useAnimatedStyle(() => { - const translateX = scrollX.value; - const cardOffset = index * (CARD_WIDTH + 16); - const distance = Math.abs(translateX - cardOffset); - const maxDistance = (CARD_WIDTH + 16) * 0.5; // Smaller threshold for smoother transition - - // Hide genres when scrolling (not centered) - const progress = Math.min(distance / maxDistance, 1); - const opacity = 1 - progress; // Linear fade out - const clampedOpacity = Math.max(0, Math.min(1, opacity)); - - return { - opacity: clampedOpacity, - }; - }); + // ULTRA-OPTIMIZED: Only animate the center card and ±1 neighbors + // Use a simple distance-based approach instead of reading scrollX.value during render + const shouldAnimate = useMemo(() => { + // For now, animate all cards but with early exit in worklets + // This avoids reading scrollX.value during render + return true; + }, [index]); - const actionsAnimatedStyle = useAnimatedStyle(() => { + // Combined animation for genres and actions (same calculation) + const overlayAnimatedStyle = useAnimatedStyle(() => { const translateX = scrollX.value; const cardOffset = index * (CARD_WIDTH + 16); const distance = Math.abs(translateX - cardOffset); - const maxDistance = (CARD_WIDTH + 16) * 0.5; // Smaller threshold for smoother transition - // Hide actions when scrolling (not centered) + // AGGRESSIVE early exit for cards far from center + if (distance > (CARD_WIDTH + 16) * 1.2) { + return { opacity: 0 }; + } + + const maxDistance = (CARD_WIDTH + 16) * 0.5; const progress = Math.min(distance / maxDistance, 1); - const opacity = 1 - progress; // Linear fade out + const opacity = 1 - progress; const clampedOpacity = Math.max(0, Math.min(1, opacity)); return { @@ -415,11 +376,20 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail }; }); - // Scroll-based animations + // ULTRA-OPTIMIZED: Only animate center card and ±1 neighbors const cardAnimatedStyle = useAnimatedStyle(() => { const translateX = scrollX.value; const cardOffset = index * (CARD_WIDTH + 16); const distance = Math.abs(translateX - cardOffset); + + // AGGRESSIVE early exit for cards far from center + if (distance > (CARD_WIDTH + 16) * 1.5) { + return { + transform: [{ scale: 0.9 }], + opacity: 0.7 + }; + } + const maxDistance = CARD_WIDTH + 16; // Scale animation based on distance from center @@ -436,38 +406,40 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail }; }); - const bannerParallaxStyle = useAnimatedStyle(() => { - const translateX = scrollX.value; - const cardOffset = index * (CARD_WIDTH + 16); - const distance = translateX - cardOffset; - - // Reduced parallax effect to prevent displacement - const parallaxOffset = distance * 0.05; - - return { - transform: [{ translateX: parallaxOffset }], - }; - }); + // TEMPORARILY DISABLED FOR PERFORMANCE TESTING + // const bannerParallaxStyle = useAnimatedStyle(() => { + // const translateX = scrollX.value; + // const cardOffset = index * (CARD_WIDTH + 16); + // const distance = translateX - cardOffset; + // + // // Reduced parallax effect to prevent displacement + // const parallaxOffset = distance * 0.05; + // + // return { + // transform: [{ translateX: parallaxOffset }], + // }; + // }); - const infoParallaxStyle = useAnimatedStyle(() => { - const translateX = scrollX.value; - const cardOffset = index * (CARD_WIDTH + 16); - const distance = Math.abs(translateX - cardOffset); - const maxDistance = CARD_WIDTH + 16; - - // Hide info section when scrolling (not centered) - const progress = distance / maxDistance; - const opacity = 1 - progress * 2; // Fade out faster when scrolling - const clampedOpacity = Math.max(0, Math.min(1, opacity)); - - // Minimal parallax for info section to prevent displacement - const parallaxOffset = -(translateX - cardOffset) * 0.02; - - return { - transform: [{ translateY: parallaxOffset }], - opacity: clampedOpacity, - }; - }); + // TEMPORARILY DISABLED FOR PERFORMANCE TESTING + // const infoParallaxStyle = useAnimatedStyle(() => { + // const translateX = scrollX.value; + // const cardOffset = index * (CARD_WIDTH + 16); + // const distance = Math.abs(translateX - cardOffset); + // const maxDistance = CARD_WIDTH + 16; + // + // // Hide info section when scrolling (not centered) + // const progress = distance / maxDistance; + // const opacity = 1 - progress * 2; // Fade out faster when scrolling + // const clampedOpacity = Math.max(0, Math.min(1, opacity)); + // + // // Minimal parallax for info section to prevent displacement + // const parallaxOffset = -(translateX - cardOffset) * 0.02; + // + // return { + // transform: [{ translateY: parallaxOffset }], + // opacity: clampedOpacity, + // }; + // }); useEffect(() => { if (bannerLoaded) { @@ -488,9 +460,8 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail }, [logoLoaded]); return ( - = memo(({ item, colors, logoFail {!bannerLoaded && ( )} - + = memo(({ item, colors, logoFail {item.genres.slice(0, 3).join(' • ')} @@ -542,29 +513,6 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail )} - {/* Static action buttons positioned absolutely over the card */} - - - - - - Play - - - - Info - - - - {/* Static logo positioned absolutely over the card */} {item.logo && !logoFailed && ( @@ -594,7 +542,7 @@ const CarouselCard: React.FC = memo(({ item, colors, logoFail ) : null} - + ); }); @@ -731,31 +679,6 @@ const styles = StyleSheet.create({ height: 64, marginBottom: 6, }, - playButton: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingVertical: 10, - borderRadius: 24, - }, - playText: { - fontWeight: '700', - marginLeft: 6, - fontSize: 14, - }, - secondaryButton: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 14, - paddingVertical: 9, - borderRadius: 22, - borderWidth: 1, - }, - secondaryText: { - fontWeight: '600', - marginLeft: 6, - fontSize: 14, - }, logoOverlay: { position: 'absolute', left: 0, @@ -764,7 +687,7 @@ const styles = StyleSheet.create({ bottom: 0, alignItems: 'center', justifyContent: 'flex-end', - paddingBottom: 80, // Position above genres and actions + paddingBottom: 40, // Position above genres }, titleOverlay: { position: 'absolute', @@ -774,19 +697,9 @@ const styles = StyleSheet.create({ bottom: 0, alignItems: 'center', justifyContent: 'flex-end', - paddingBottom: 90, // Position above genres and actions + paddingBottom: 50, // Position above genres }, genresOverlay: { - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - alignItems: 'center', - justifyContent: 'flex-end', - paddingBottom: 65, // Position above actions - }, - actionsOverlay: { position: 'absolute', left: 0, right: 0, diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index c432e2a..2363171 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -9,6 +9,7 @@ import { StatusBar, useColorScheme, Dimensions, + useWindowDimensions, ImageBackground, ScrollView, Platform, @@ -389,7 +390,10 @@ const HomeScreen = () => { // Allow free rotation on tablets; lock portrait on phones try { - const isTabletDevice = Platform.OS === 'ios' ? (Platform as any).isPad === true : isTablet; + // Use device physical characteristics, not current orientation + const isTabletDevice = Platform.OS === 'ios' + ? (Platform as any).isPad === true + : Math.min(windowWidth, Dimensions.get('screen').height) >= 768; if (isTabletDevice) { ScreenOrientation.unlockAsync(); } else { @@ -616,11 +620,11 @@ const HomeScreen = () => { // Stable keyExtractor for FlashList const keyExtractor = useCallback((item: HomeScreenListItem) => item.key, []); - // Memoize device check to avoid repeated Dimensions.get calls + // Use reactive window dimensions that update on orientation changes + const { width: windowWidth } = useWindowDimensions(); const isTablet = useMemo(() => { - const deviceWidth = Dimensions.get('window').width; - return deviceWidth >= 768; - }, []); + return windowWidth >= 768; + }, [windowWidth]); // Memoize individual section components to prevent re-renders const memoizedFeaturedContent = useMemo(() => { @@ -640,7 +644,7 @@ const HomeScreen = () => { loading={featuredLoading} /> ); - }, [isTablet, settings.heroStyle, showHeroSection, featuredContentSource, featuredContent, allFeaturedContent, isSaved, handleSaveToLibrary, featuredLoading]); + }, [isTablet, settings.heroStyle, showHeroSection, featuredContentSource, featuredLoading]); const memoizedThisWeekSection = useMemo(() => , []); const memoizedContinueWatchingSection = useMemo(() => , []);