From 93a016b96eb713ce100ef75865d36dd908d5f5ff Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 20 Jun 2025 17:37:34 +0530 Subject: [PATCH] Enhance navigation animations and transitions for improved user experience This update modifies the AppNavigator to refine screen transition animations, including adjustments to animation types and durations based on platform. The SearchScreen now features smoother keyboard dismissal before navigation, and animations are added to enhance the visual experience during screen transitions. Additionally, the StreamsScreen is updated to prevent animation conflicts during exits, improving overall responsiveness and clarity in user interactions. --- src/navigation/AppNavigator.tsx | 32 +++++++-- src/screens/SearchScreen.tsx | 20 +++++- src/screens/StreamsScreen.tsx | 113 +++++++++++++++++++++++++++++--- 3 files changed, 149 insertions(+), 16 deletions(-) diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 1c1cec2c3..86d41b1f8 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -780,8 +780,10 @@ const AppNavigator = () => { component={StreamsScreen as any} options={{ headerShown: false, - animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'fade_from_bottom', - animationDuration: Platform.OS === 'android' ? 200 : 300, + animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none', + animationDuration: Platform.OS === 'android' ? 0 : 300, + gestureEnabled: true, + gestureDirection: Platform.OS === 'ios' ? 'vertical' : 'horizontal', ...(Platform.OS === 'ios' && { presentation: 'modal' }), contentStyle: { backgroundColor: currentTheme.colors.darkBackground, @@ -825,8 +827,30 @@ const AppNavigator = () => { name="Search" component={SearchScreen as any} options={{ - animation: 'fade', - animationDuration: Platform.OS === 'android' ? 300 : 350, + animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade', + animationDuration: Platform.OS === 'android' ? 250 : 350, + gestureEnabled: true, + gestureDirection: 'horizontal', + ...(Platform.OS === 'android' && { + cardStyleInterpolator: ({ current, layouts }: any) => { + return { + cardStyle: { + transform: [ + { + translateX: current.progress.interpolate({ + inputRange: [0, 1], + outputRange: [layouts.screen.width, 0], + }), + }, + ], + opacity: current.progress.interpolate({ + inputRange: [0, 0.3, 1], + outputRange: [0, 0.85, 1], + }), + }, + }; + }, + }), contentStyle: { backgroundColor: currentTheme.colors.darkBackground, }, diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx index 2d57e6576..7ba205bd8 100644 --- a/src/screens/SearchScreen.tsx +++ b/src/screens/SearchScreen.tsx @@ -287,7 +287,14 @@ const SearchScreen = () => { setShowRecent(true); loadRecentSearches(); } else { - navigation.goBack(); + // Add a small delay to allow keyboard to dismiss smoothly before navigation + if (Platform.OS === 'android') { + setTimeout(() => { + navigation.goBack(); + }, 100); + } else { + navigation.goBack(); + } } }; @@ -497,7 +504,14 @@ const SearchScreen = () => { const headerHeight = headerBaseHeight + topSpacing + 60; return ( - + { )} - + ); }; diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index d7f2f5983..c3750c5c7 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -33,6 +33,7 @@ import { useSettings } from '../hooks/useSettings'; import QualityBadge from '../components/metadata/QualityBadge'; import Animated, { FadeIn, + FadeOut, FadeInDown, SlideInDown, withSpring, @@ -55,13 +56,14 @@ const DOLBY_ICON = 'https://upload.wikimedia.org/wikipedia/en/thumb/3/3f/Dolby_V const { width, height } = Dimensions.get('window'); // Extracted Components -const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }: { +const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme, isExiting }: { stream: Stream; onPress: () => void; index: number; isLoading?: boolean; statusMessage?: string; theme: any; + isExiting?: boolean; }) => { const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); @@ -78,13 +80,92 @@ const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }: const displayTitle = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream'); const displayAddonName = isHDRezka ? '' : (stream.title || ''); - // Animation delay based on index - stagger effect - const enterDelay = 100 + (index * 50); + // Animation delay based on index - stagger effect (only if not exiting) + const enterDelay = isExiting ? 0 : 100 + (index * 30); + + // Use simple View when exiting to prevent animation conflicts + if (isExiting) { + return ( + + + + + + + {displayTitle} + + {displayAddonName && displayAddonName !== displayTitle && ( + + {displayAddonName} + + )} + + + {/* Show loading indicator if stream is loading */} + {isLoading && ( + + + + {statusMessage || "Loading..."} + + + )} + + + + {quality && quality >= "720" && ( + + )} + + {isDolby && ( + + )} + + {size && ( + + {size} + + )} + + {isDebrid && ( + + DEBRID + + )} + + {/* Special badge for HDRezka streams */} + {isHDRezka && ( + + HDREZKA + + )} + + + + + + + + + ); + } return ( { // Add state for handling orientation transition const [isTransitioning, setIsTransitioning] = useState(false); + + // Add state to prevent animation conflicts during exit + const [isExiting, setIsExiting] = useState(false); // Add timing logs const [loadStartTime, setLoadStartTime] = useState(0); @@ -400,20 +484,25 @@ export const StreamsScreen = () => { // Memoize handlers const handleBack = useCallback(() => { + // Set exit state to prevent animation conflicts and hide content immediately + setIsExiting(true); + const cleanup = () => { - headerOpacity.value = withTiming(0, { duration: 200 }); - heroScale.value = withTiming(0.95, { duration: 200 }); - filterOpacity.value = withTiming(0, { duration: 200 }); + headerOpacity.value = withTiming(0, { duration: 100 }); + heroScale.value = withTiming(0.95, { duration: 100 }); + filterOpacity.value = withTiming(0, { duration: 100 }); }; cleanup(); // For series episodes, always replace current screen with metadata screen if (type === 'series') { + // Immediate navigation for series navigation.replace('Metadata', { id: id, type: type }); } else { + // Immediate navigation for movies navigation.goBack(); } }, [navigation, headerOpacity, heroScale, filterOpacity, type, id]); @@ -954,9 +1043,10 @@ export const StreamsScreen = () => { isLoading={isLoading} statusMessage={undefined} theme={currentTheme} + isExiting={isExiting} /> ); - }, [handleStreamPress, currentTheme]); + }, [handleStreamPress, currentTheme, isExiting]); const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => { const isProviderLoading = loadingProviders[section.addonId]; @@ -1035,6 +1125,11 @@ export const StreamsScreen = () => { barStyle="light-content" /> + {/* Instant overlay when exiting to prevent glitches */} + {isExiting && ( + + )} + {/* Transition overlay to mask orientation changes */} {isTransitioning && (