orientation optimization

This commit is contained in:
tapframe 2025-10-21 17:49:49 +05:30
parent 614ffc12c0
commit c852c56231
2 changed files with 104 additions and 187 deletions

View file

@ -19,7 +19,6 @@ if (Platform.OS === 'ios') {
liquidGlassAvailable = false; liquidGlassAvailable = false;
} }
} }
import { MaterialIcons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
@ -44,7 +43,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { settings } = useSettings(); 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 [activeIndex, setActiveIndex] = useState(0);
const [failedLogoIds, setFailedLogoIds] = useState<Set<string>>(new Set()); const [failedLogoIds, setFailedLogoIds] = useState<Set<string>>(new Set());
const scrollViewRef = useRef<any>(null); const scrollViewRef = useRef<any>(null);
@ -102,7 +101,8 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ 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( useAnimatedReaction(
() => { () => {
const idx = Math.round(scrollX.value / interval); const idx = Math.round(scrollX.value / interval);
@ -110,6 +110,12 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
}, },
(idx, prevIdx) => { (idx, prevIdx) => {
if (idx == null || idx === prevIdx) return; 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 // Clamp to bounds to avoid out-of-range access
const clamped = Math.max(0, Math.min(idx, data.length - 1)); const clamped = Math.max(0, Math.min(idx, data.length - 1));
runOnJS(setActiveIndex)(clamped); runOnJS(setActiveIndex)(clamped);
@ -123,23 +129,20 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
navigation.navigate('Metadata', { id, type }); navigation.navigate('Metadata', { id, type });
}, [navigation]); }, [navigation]);
const handleNavigateToStreams = useCallback((id: string, type: any) => {
navigation.navigate('Streams', { id, type });
}, [navigation]);
// Container animation based on scroll - must be before early returns // Container animation based on scroll - must be before early returns
const containerAnimatedStyle = useAnimatedStyle(() => { // TEMPORARILY DISABLED FOR PERFORMANCE TESTING
const translateX = scrollX.value; // const containerAnimatedStyle = useAnimatedStyle(() => {
const progress = Math.abs(translateX) / (data.length * (CARD_WIDTH + 16)); // 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; // // Very subtle scale animation for the entire container
const clampedScale = Math.max(0.99, Math.min(1, scale)); // const scale = 1 - progress * 0.01;
// const clampedScale = Math.max(0.99, Math.min(1, scale));
return { //
transform: [{ scale: clampedScale }], // return {
}; // transform: [{ scale: clampedScale }],
}); // };
// });
if (loading) { if (loading) {
return ( return (
@ -193,18 +196,6 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
item: StreamingContent; item: StreamingContent;
insets: any; 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 ( return (
<View <View
style={[ style={[
@ -213,9 +204,9 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
] as StyleProp<ViewStyle>} ] as StyleProp<ViewStyle>}
pointerEvents="none" pointerEvents="none"
> >
<Animated.View <View
key={item.id} key={item.id}
style={[animatedStyle, { flex: 1 }] as any} style={{ flex: 1 } as any}
> >
{Platform.OS === 'android' ? ( {Platform.OS === 'android' ? (
<Image <Image
@ -254,7 +245,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
locations={[0.4, 1]} locations={[0.4, 1]}
style={styles.backgroundOverlay as ViewStyle} style={styles.backgroundOverlay as ViewStyle}
/> />
</Animated.View> </View>
</View> </View>
); );
}); });
@ -263,33 +254,8 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
return ( return (
<Animated.View entering={FadeIn.duration(350).easing(Easing.out(Easing.cubic))}> <Animated.View entering={FadeIn.duration(350).easing(Easing.out(Easing.cubic))}>
<Animated.View style={[styles.container as ViewStyle, containerAnimatedStyle]}> <Animated.View style={[styles.container as ViewStyle]}>
{settings.enableHomeHeroBackground && data.length > 0 && ( {/* Removed preload images for performance - let FastImage cache handle it naturally */}
<View style={{ height: 0, width: 0, overflow: 'hidden' }}>
{data[activeIndex + 1] && (
<FastImage
source={{
uri: data[activeIndex + 1].banner || data[activeIndex + 1].poster,
priority: FastImage.priority.low,
cache: FastImage.cacheControl.immutable
}}
style={{ width: 1, height: 1 }}
resizeMode={FastImage.resizeMode.cover}
/>
)}
{activeIndex > 0 && data[activeIndex - 1] && (
<FastImage
source={{
uri: data[activeIndex - 1].banner || data[activeIndex - 1].poster,
priority: FastImage.priority.low,
cache: FastImage.cacheControl.immutable
}}
style={{ width: 1, height: 1 }}
resizeMode={FastImage.resizeMode.cover}
/>
)}
</View>
)}
{settings.enableHomeHeroBackground && data[activeIndex] && ( {settings.enableHomeHeroBackground && data[activeIndex] && (
<BackgroundImage <BackgroundImage
item={data[activeIndex]} item={data[activeIndex]}
@ -313,7 +279,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
decelerationRate="fast" decelerationRate="fast"
contentContainerStyle={contentPadding} contentContainerStyle={contentPadding}
onScroll={scrollHandler} onScroll={scrollHandler}
scrollEventThrottle={8} scrollEventThrottle={32}
disableIntervalMomentum disableIntervalMomentum
pagingEnabled={false} pagingEnabled={false}
bounces={false} bounces={false}
@ -327,7 +293,6 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
logoFailed={failedLogoIds.has(item.id)} logoFailed={failedLogoIds.has(item.id)}
onLogoError={() => setFailedLogoIds((prev) => new Set(prev).add(item.id))} onLogoError={() => setFailedLogoIds((prev) => new Set(prev).add(item.id))}
onPressInfo={() => handleNavigateToMetadata(item.id, item.type)} onPressInfo={() => handleNavigateToMetadata(item.id, item.type)}
onPressPlay={() => handleNavigateToStreams(item.id, item.type)}
scrollX={scrollX} scrollX={scrollX}
index={index} index={index}
/> />
@ -343,13 +308,12 @@ interface CarouselCardProps {
colors: any; colors: any;
logoFailed: boolean; logoFailed: boolean;
onLogoError: () => void; onLogoError: () => void;
onPressPlay: () => void;
onPressInfo: () => void; onPressInfo: () => void;
scrollX: SharedValue<number>; scrollX: SharedValue<number>;
index: number; index: number;
} }
const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFailed, onLogoError, onPressPlay, onPressInfo, scrollX, index }) => { const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index }) => {
const [bannerLoaded, setBannerLoaded] = useState(false); const [bannerLoaded, setBannerLoaded] = useState(false);
const [logoLoaded, setLogoLoaded] = useState(false); const [logoLoaded, setLogoLoaded] = useState(false);
@ -383,31 +347,28 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
opacity: logoOpacity.value, opacity: logoOpacity.value,
})); }));
const genresAnimatedStyle = useAnimatedStyle(() => { // ULTRA-OPTIMIZED: Only animate the center card and ±1 neighbors
const translateX = scrollX.value; // Use a simple distance-based approach instead of reading scrollX.value during render
const cardOffset = index * (CARD_WIDTH + 16); const shouldAnimate = useMemo(() => {
const distance = Math.abs(translateX - cardOffset); // For now, animate all cards but with early exit in worklets
const maxDistance = (CARD_WIDTH + 16) * 0.5; // Smaller threshold for smoother transition // This avoids reading scrollX.value during render
return true;
// Hide genres when scrolling (not centered) }, [index]);
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,
};
});
const actionsAnimatedStyle = useAnimatedStyle(() => { // Combined animation for genres and actions (same calculation)
const overlayAnimatedStyle = useAnimatedStyle(() => {
const translateX = scrollX.value; const translateX = scrollX.value;
const cardOffset = index * (CARD_WIDTH + 16); const cardOffset = index * (CARD_WIDTH + 16);
const distance = Math.abs(translateX - cardOffset); 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 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)); const clampedOpacity = Math.max(0, Math.min(1, opacity));
return { return {
@ -415,11 +376,20 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
}; };
}); });
// Scroll-based animations // ULTRA-OPTIMIZED: Only animate center card and ±1 neighbors
const cardAnimatedStyle = useAnimatedStyle(() => { const cardAnimatedStyle = useAnimatedStyle(() => {
const translateX = scrollX.value; const translateX = scrollX.value;
const cardOffset = index * (CARD_WIDTH + 16); const cardOffset = index * (CARD_WIDTH + 16);
const distance = Math.abs(translateX - cardOffset); 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; const maxDistance = CARD_WIDTH + 16;
// Scale animation based on distance from center // Scale animation based on distance from center
@ -436,38 +406,40 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
}; };
}); });
const bannerParallaxStyle = useAnimatedStyle(() => { // TEMPORARILY DISABLED FOR PERFORMANCE TESTING
const translateX = scrollX.value; // const bannerParallaxStyle = useAnimatedStyle(() => {
const cardOffset = index * (CARD_WIDTH + 16); // const translateX = scrollX.value;
const distance = translateX - cardOffset; // const cardOffset = index * (CARD_WIDTH + 16);
// const distance = translateX - cardOffset;
// Reduced parallax effect to prevent displacement //
const parallaxOffset = distance * 0.05; // // Reduced parallax effect to prevent displacement
// const parallaxOffset = distance * 0.05;
return { //
transform: [{ translateX: parallaxOffset }], // return {
}; // transform: [{ translateX: parallaxOffset }],
}); // };
// });
const infoParallaxStyle = useAnimatedStyle(() => { // TEMPORARILY DISABLED FOR PERFORMANCE TESTING
const translateX = scrollX.value; // const infoParallaxStyle = useAnimatedStyle(() => {
const cardOffset = index * (CARD_WIDTH + 16); // const translateX = scrollX.value;
const distance = Math.abs(translateX - cardOffset); // const cardOffset = index * (CARD_WIDTH + 16);
const maxDistance = 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; // // Hide info section when scrolling (not centered)
const opacity = 1 - progress * 2; // Fade out faster when scrolling // const progress = distance / maxDistance;
const clampedOpacity = Math.max(0, Math.min(1, opacity)); // 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; // // Minimal parallax for info section to prevent displacement
// const parallaxOffset = -(translateX - cardOffset) * 0.02;
return { //
transform: [{ translateY: parallaxOffset }], // return {
opacity: clampedOpacity, // transform: [{ translateY: parallaxOffset }],
}; // opacity: clampedOpacity,
}); // };
// });
useEffect(() => { useEffect(() => {
if (bannerLoaded) { if (bannerLoaded) {
@ -488,9 +460,8 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
}, [logoLoaded]); }, [logoLoaded]);
return ( return (
<Animated.View <View
style={{ width: CARD_WIDTH + 16 }} style={{ width: CARD_WIDTH + 16 }}
entering={FadeIn.duration(400).delay(index * 100).easing(Easing.out(Easing.cubic))}
> >
<TouchableOpacity <TouchableOpacity
activeOpacity={0.9} activeOpacity={0.9}
@ -510,7 +481,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
{!bannerLoaded && ( {!bannerLoaded && (
<View style={styles.skeletonBannerFull as ViewStyle} /> <View style={styles.skeletonBannerFull as ViewStyle} />
)} )}
<Animated.View style={[bannerAnimatedStyle, bannerParallaxStyle, { flex: 1 }]}> <Animated.View style={[bannerAnimatedStyle, { flex: 1 }]}>
<FastImage <FastImage
source={{ source={{
uri: item.banner || item.poster, uri: item.banner || item.poster,
@ -534,7 +505,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
<View style={styles.genresOverlay as ViewStyle} pointerEvents="none"> <View style={styles.genresOverlay as ViewStyle} pointerEvents="none">
<Animated.View entering={FadeIn.duration(400).delay(100)}> <Animated.View entering={FadeIn.duration(400).delay(100)}>
<Animated.Text <Animated.Text
style={[styles.genres as TextStyle, { color: colors.mediumEmphasis, textAlign: 'center' }, genresAnimatedStyle]} style={[styles.genres as TextStyle, { color: colors.mediumEmphasis, textAlign: 'center' }, overlayAnimatedStyle]}
numberOfLines={1} numberOfLines={1}
> >
{item.genres.slice(0, 3).join(' • ')} {item.genres.slice(0, 3).join(' • ')}
@ -542,29 +513,6 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
</Animated.View> </Animated.View>
</View> </View>
)} )}
{/* Static action buttons positioned absolutely over the card */}
<View style={styles.actionsOverlay as ViewStyle} pointerEvents="box-none">
<Animated.View entering={FadeIn.duration(500).delay(200)}>
<Animated.View style={[styles.actions as ViewStyle, actionsAnimatedStyle]}>
<TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: colors.white }]}
onPress={onPressPlay}
activeOpacity={0.85}
>
<MaterialIcons name="play-arrow" size={22} color={colors.black} />
<Text style={[styles.playText as TextStyle, { color: colors.black }]}>Play</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.secondaryButton as ViewStyle, { borderColor: 'rgba(255,255,255,0.25)' }]}
onPress={onPressInfo}
activeOpacity={0.8}
>
<MaterialIcons name="info-outline" size={18} color={colors.white} />
<Text style={[styles.secondaryText as TextStyle, { color: colors.white }]}>Info</Text>
</TouchableOpacity>
</Animated.View>
</Animated.View>
</View>
{/* Static logo positioned absolutely over the card */} {/* Static logo positioned absolutely over the card */}
{item.logo && !logoFailed && ( {item.logo && !logoFailed && (
<View style={styles.logoOverlay as ViewStyle} pointerEvents="none"> <View style={styles.logoOverlay as ViewStyle} pointerEvents="none">
@ -594,7 +542,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
</View> </View>
) : null} ) : null}
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </View>
); );
}); });
@ -731,31 +679,6 @@ const styles = StyleSheet.create({
height: 64, height: 64,
marginBottom: 6, 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: { logoOverlay: {
position: 'absolute', position: 'absolute',
left: 0, left: 0,
@ -764,7 +687,7 @@ const styles = StyleSheet.create({
bottom: 0, bottom: 0,
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-end', justifyContent: 'flex-end',
paddingBottom: 80, // Position above genres and actions paddingBottom: 40, // Position above genres
}, },
titleOverlay: { titleOverlay: {
position: 'absolute', position: 'absolute',
@ -774,19 +697,9 @@ const styles = StyleSheet.create({
bottom: 0, bottom: 0,
alignItems: 'center', alignItems: 'center',
justifyContent: 'flex-end', justifyContent: 'flex-end',
paddingBottom: 90, // Position above genres and actions paddingBottom: 50, // Position above genres
}, },
genresOverlay: { genresOverlay: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'flex-end',
paddingBottom: 65, // Position above actions
},
actionsOverlay: {
position: 'absolute', position: 'absolute',
left: 0, left: 0,
right: 0, right: 0,

View file

@ -9,6 +9,7 @@ import {
StatusBar, StatusBar,
useColorScheme, useColorScheme,
Dimensions, Dimensions,
useWindowDimensions,
ImageBackground, ImageBackground,
ScrollView, ScrollView,
Platform, Platform,
@ -389,7 +390,10 @@ const HomeScreen = () => {
// Allow free rotation on tablets; lock portrait on phones // Allow free rotation on tablets; lock portrait on phones
try { 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) { if (isTabletDevice) {
ScreenOrientation.unlockAsync(); ScreenOrientation.unlockAsync();
} else { } else {
@ -616,11 +620,11 @@ const HomeScreen = () => {
// Stable keyExtractor for FlashList // Stable keyExtractor for FlashList
const keyExtractor = useCallback((item: HomeScreenListItem) => item.key, []); 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 isTablet = useMemo(() => {
const deviceWidth = Dimensions.get('window').width; return windowWidth >= 768;
return deviceWidth >= 768; }, [windowWidth]);
}, []);
// Memoize individual section components to prevent re-renders // Memoize individual section components to prevent re-renders
const memoizedFeaturedContent = useMemo(() => { const memoizedFeaturedContent = useMemo(() => {
@ -640,7 +644,7 @@ const HomeScreen = () => {
loading={featuredLoading} loading={featuredLoading}
/> />
); );
}, [isTablet, settings.heroStyle, showHeroSection, featuredContentSource, featuredContent, allFeaturedContent, isSaved, handleSaveToLibrary, featuredLoading]); }, [isTablet, settings.heroStyle, showHeroSection, featuredContentSource, featuredLoading]);
const memoizedThisWeekSection = useMemo(() => <ThisWeekSection />, []); const memoizedThisWeekSection = useMemo(() => <ThisWeekSection />, []);
const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []); const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);