Enhance FeaturedContent component with improved animations and content transition handling. Introduce new animation states for poster scaling, overlay opacity, and content visibility during content changes. Refactor image loading logic to support smoother transitions and add a subtle content overlay for better readability. Update styles for improved layout and proportions.

This commit is contained in:
tapframe 2025-06-08 20:01:27 +05:30
parent 076446aac6
commit 19bb898b6b

View file

@ -64,9 +64,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
// Add a ref to track logo fetch in progress
const logoFetchInProgress = useRef<boolean>(false);
// Enhanced poster transition animations
const posterScale = useSharedValue(1);
const posterTranslateY = useSharedValue(0);
const overlayOpacity = useSharedValue(0.15);
// Animation values
const posterAnimatedStyle = useAnimatedStyle(() => ({
opacity: posterOpacity.value,
transform: [
{ scale: posterScale.value },
{ translateY: posterTranslateY.value }
],
}));
const logoAnimatedStyle = useAnimatedStyle(() => ({
@ -84,6 +93,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
opacity: buttonsOpacity.value,
}));
const overlayAnimatedStyle = useAnimatedStyle(() => ({
opacity: overlayOpacity.value,
}));
// Preload the image
const preloadImage = async (url: string): Promise<boolean> => {
if (!url) return false;
@ -255,41 +268,92 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
const posterUrl = featuredContent.banner || featuredContent.poster;
const contentId = featuredContent.id;
const isContentChange = contentId !== prevContentIdRef.current;
// Reset states for new content
if (contentId !== prevContentIdRef.current) {
posterOpacity.value = 0;
// Enhanced content change detection and animations
if (isContentChange) {
// Animate out current content
if (prevContentIdRef.current) {
posterOpacity.value = withTiming(0, {
duration: 300,
easing: Easing.out(Easing.cubic)
});
posterScale.value = withTiming(0.95, {
duration: 300,
easing: Easing.out(Easing.cubic)
});
overlayOpacity.value = withTiming(0.6, {
duration: 300,
easing: Easing.out(Easing.cubic)
});
contentOpacity.value = withTiming(0.3, {
duration: 200,
easing: Easing.out(Easing.cubic)
});
buttonsOpacity.value = withTiming(0.3, {
duration: 200,
easing: Easing.out(Easing.cubic)
});
} else {
// Initial load - start from 0
posterOpacity.value = 0;
posterScale.value = 1.1;
overlayOpacity.value = 0;
contentOpacity.value = 0;
buttonsOpacity.value = 0;
}
logoOpacity.value = 0;
}
prevContentIdRef.current = contentId;
// Set poster URL immediately for instant display
// Set poster URL for immediate display
if (posterUrl) setBannerUrl(posterUrl);
// Load images in background
// Load images with enhanced animations
const loadImages = async () => {
// Load poster
// Small delay to allow fade out animation to complete
await new Promise(resolve => setTimeout(resolve, isContentChange && prevContentIdRef.current ? 300 : 0));
// Load poster with enhanced transition
if (posterUrl) {
const posterSuccess = await preloadImage(posterUrl);
if (posterSuccess) {
posterOpacity.value = withTiming(1, {
duration: 600,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
// Animate in new poster with scale and fade
posterScale.value = withTiming(1, {
duration: 800,
easing: Easing.out(Easing.cubic)
});
posterOpacity.value = withTiming(1, {
duration: 700,
easing: Easing.out(Easing.cubic)
});
overlayOpacity.value = withTiming(0.15, {
duration: 600,
easing: Easing.out(Easing.cubic)
});
// Animate content back in with delay
contentOpacity.value = withDelay(200, withTiming(1, {
duration: 600,
easing: Easing.out(Easing.cubic)
}));
buttonsOpacity.value = withDelay(400, withTiming(1, {
duration: 500,
easing: Easing.out(Easing.cubic)
}));
}
}
// Load logo if available
// Load logo if available with enhanced timing
if (logoUrl) {
const logoSuccess = await preloadImage(logoUrl);
if (logoSuccess) {
logoOpacity.value = withDelay(300, withTiming(1, {
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
logoOpacity.value = withDelay(500, withTiming(1, {
duration: 600,
easing: Easing.out(Easing.cubic)
}));
} else {
// If prefetch fails, mark as error to show title text instead
setLogoLoadError(true);
console.warn(`[FeaturedContent] Logo prefetch failed, falling back to text: ${logoUrl}`);
}
@ -304,131 +368,146 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
}
return (
<TouchableOpacity
activeOpacity={0.9}
onPress={() => {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}}
style={styles.featuredContainer as ViewStyle}
<Animated.View
entering={FadeIn.duration(800).easing(Easing.out(Easing.cubic))}
>
<Animated.View style={[styles.imageContainer, posterAnimatedStyle]}>
<ImageBackground
source={{ uri: bannerUrl || featuredContent.poster }}
style={styles.featuredImage as ViewStyle}
resizeMode="cover"
>
<LinearGradient
colors={[
'transparent',
'rgba(0,0,0,0.1)',
'rgba(0,0,0,0.7)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.3, 0.7, 1]}
style={styles.featuredGradient as ViewStyle}
<TouchableOpacity
activeOpacity={0.95}
onPress={() => {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}}
style={styles.featuredContainer as ViewStyle}
>
<Animated.View style={[styles.imageContainer, posterAnimatedStyle]}>
<ImageBackground
source={{ uri: bannerUrl || featuredContent.poster }}
style={styles.featuredImage as ViewStyle}
resizeMode="cover"
>
<Animated.View
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
{/* Subtle content overlay for better readability */}
<Animated.View style={[styles.contentOverlay, overlayAnimatedStyle]} />
<LinearGradient
colors={[
'rgba(0,0,0,0.1)',
'rgba(0,0,0,0.2)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.2, 0.5, 0.8, 1]}
style={styles.featuredGradient as ViewStyle}
>
{logoUrl && !logoLoadError ? (
<Animated.View style={logoAnimatedStyle}>
<ExpoImage
source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle}
contentFit="contain"
cachePolicy="memory-disk"
transition={400}
onError={() => {
console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`);
setLogoLoadError(true);
}}
<Animated.View
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
>
{logoUrl && !logoLoadError ? (
<Animated.View style={logoAnimatedStyle}>
<ExpoImage
source={{ uri: logoUrl }}
style={styles.featuredLogo as ImageStyle}
contentFit="contain"
cachePolicy="memory-disk"
transition={400}
onError={() => {
console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`);
setLogoLoadError(true);
}}
/>
</Animated.View>
) : (
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
{featuredContent.name}
</Text>
)}
<View style={styles.genreContainer as ViewStyle}>
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
<React.Fragment key={index}>
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
{genre}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text>
)}
</React.Fragment>
))}
</View>
</Animated.View>
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}>
<TouchableOpacity
style={styles.myListButton as ViewStyle}
onPress={handleSaveToLibrary}
>
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={currentTheme.colors.white}
/>
</Animated.View>
) : (
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
{featuredContent.name}
</Text>
)}
<View style={styles.genreContainer as ViewStyle}>
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
<React.Fragment key={index}>
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
{genre}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text>
)}
</React.Fragment>
))}
</View>
</Animated.View>
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "Save"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => {
if (featuredContent) {
navigation.navigate('Streams', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
>
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play
</Text>
</TouchableOpacity>
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}>
<TouchableOpacity
style={styles.myListButton as ViewStyle}
onPress={handleSaveToLibrary}
>
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={currentTheme.colors.white}
/>
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "Save"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => {
if (featuredContent) {
navigation.navigate('Streams', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
>
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.infoButton as ViewStyle}
onPress={() => {
if (featuredContent) {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
>
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info
</Text>
</TouchableOpacity>
</Animated.View>
</LinearGradient>
</ImageBackground>
</Animated.View>
</TouchableOpacity>
<TouchableOpacity
style={styles.infoButton as ViewStyle}
onPress={() => {
if (featuredContent) {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
>
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info
</Text>
</TouchableOpacity>
</Animated.View>
</LinearGradient>
</ImageBackground>
</Animated.View>
</TouchableOpacity>
</Animated.View>
);
};
const styles = StyleSheet.create({
featuredContainer: {
width: '100%',
height: height * 0.48,
height: height * 0.55, // Slightly taller for better proportions
marginTop: 0,
marginBottom: 8,
marginBottom: 12,
position: 'relative',
borderRadius: 12,
overflow: 'hidden',
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
},
imageContainer: {
width: '100%',
@ -443,6 +522,7 @@ const styles = StyleSheet.create({
featuredImage: {
width: '100%',
height: '100%',
transform: [{ scale: 1.05 }], // Subtle zoom for depth
},
backgroundFallback: {
position: 'absolute',
@ -458,12 +538,14 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
justifyContent: 'space-between',
paddingTop: 20,
},
featuredContentContainer: {
flex: 1,
justifyContent: 'flex-end',
paddingHorizontal: 16,
paddingBottom: 4,
paddingHorizontal: 20,
paddingBottom: 8,
paddingTop: 40,
},
featuredLogo: {
width: width * 0.7,
@ -502,19 +584,20 @@ const styles = StyleSheet.create({
},
featuredButtons: {
flexDirection: 'row',
alignItems: 'flex-end',
alignItems: 'center',
justifyContent: 'space-evenly',
width: '100%',
flex: 1,
maxHeight: 55,
paddingTop: 0,
minHeight: 70,
paddingTop: 12,
paddingBottom: 20,
paddingHorizontal: 8,
},
playButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
paddingHorizontal: 32,
paddingVertical: 12,
paddingHorizontal: 28,
borderRadius: 30,
elevation: 4,
shadowColor: '#000',
@ -522,7 +605,7 @@ const styles = StyleSheet.create({
shadowOpacity: 0.3,
shadowRadius: 4,
flex: 0,
width: 150,
width: 140,
},
myListButton: {
flexDirection: 'column',
@ -557,6 +640,15 @@ const styles = StyleSheet.create({
fontSize: 12,
fontWeight: '500',
},
contentOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.15)',
zIndex: 1,
},
});
export default FeaturedContent;