parallax added

This commit is contained in:
tapframe 2025-11-08 14:14:33 +05:30
parent aa0c338c05
commit 426e936740
3 changed files with 92 additions and 15 deletions

View file

@ -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<number>; // 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<AppleTVHeroProps> = ({
allFeaturedContent,
loading,
onRetry,
scrollY: externalScrollY,
}) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isFocused = useIsFocused();
@ -94,6 +98,10 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
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<AppleTVHeroProps> = ({
};
});
// 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<AppleTVHeroProps> = ({
{/* Background Images with Crossfade */}
<View style={styles.backgroundContainer}>
{/* Current Image - Always visible as base */}
<View style={styles.imageWrapper}>
<Animated.View style={[styles.imageWrapper, backgroundParallaxStyle]}>
<FastImage
source={{
uri: bannerUrl,
@ -596,11 +660,11 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
resizeMode={FastImage.resizeMode.cover}
onLoad={() => setBannerLoaded((prev) => ({ ...prev, [currentIndex]: true }))}
/>
</View>
</Animated.View>
{/* Next/Preview Image - Animated overlay during drag */}
{nextIndex !== currentIndex && (
<Animated.View style={[styles.imageWrapperAbsolute, nextImageStyle]}>
<Animated.View style={[styles.imageWrapperAbsolute, nextImageStyle, backgroundParallaxStyle]}>
<FastImage
source={{
uri: nextBannerUrl,
@ -634,7 +698,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
{/* Visible trailer player - 60% height with 5% zoom and smooth fade */}
{settings?.showTrailers && trailerUrl && !trailerLoading && !trailerError && trailerPreloaded && (
<Animated.View style={trailerContainerStyle}>
<Animated.View style={[trailerContainerStyle, trailerParallaxStyle]}>
<Animated.View style={trailerVideoStyle}>
<TrailerPlayer
key={`visible-${trailerUrl}`}
@ -692,7 +756,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
{settings?.showTrailers && trailerReady && trailerUrl && (
<Animated.View style={{
position: 'absolute',
top: Platform.OS === 'android' ? 60 : 70,
top: (Platform.OS === 'android' ? 60 : 70) + insets.top,
right: 24,
zIndex: 1000,
opacity: trailerOpacity,

View file

@ -100,7 +100,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
preferredPlayer: 'internal',
showHeroSection: true,
featuredContentSource: 'catalogs',
heroStyle: 'carousel',
heroStyle: 'appletv',
selectedHeroCatalogs: [], // Empty array means all catalogs are selected
logoSourcePreference: 'tmdb', // Default to TMDB as first source
tmdbLanguagePreference: 'en', // Default to English

View file

@ -30,7 +30,7 @@ import { Stream } from '../types/metadata';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image';
import Animated, { FadeIn, Layout } from 'react-native-reanimated';
import Animated, { FadeIn, Layout, useSharedValue, useAnimatedScrollHandler } from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';
import {
Gesture,
@ -126,6 +126,9 @@ const HomeScreen = () => {
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(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(() => {