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.
This commit is contained in:
tapframe 2025-06-20 17:37:34 +05:30
parent 026c425660
commit 93a016b96e
3 changed files with 149 additions and 16 deletions

View file

@ -780,8 +780,10 @@ const AppNavigator = () => {
component={StreamsScreen as any} component={StreamsScreen as any}
options={{ options={{
headerShown: false, headerShown: false,
animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'fade_from_bottom', animation: Platform.OS === 'ios' ? 'slide_from_bottom' : 'none',
animationDuration: Platform.OS === 'android' ? 200 : 300, animationDuration: Platform.OS === 'android' ? 0 : 300,
gestureEnabled: true,
gestureDirection: Platform.OS === 'ios' ? 'vertical' : 'horizontal',
...(Platform.OS === 'ios' && { presentation: 'modal' }), ...(Platform.OS === 'ios' && { presentation: 'modal' }),
contentStyle: { contentStyle: {
backgroundColor: currentTheme.colors.darkBackground, backgroundColor: currentTheme.colors.darkBackground,
@ -825,8 +827,30 @@ const AppNavigator = () => {
name="Search" name="Search"
component={SearchScreen as any} component={SearchScreen as any}
options={{ options={{
animation: 'fade', animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
animationDuration: Platform.OS === 'android' ? 300 : 350, 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: { contentStyle: {
backgroundColor: currentTheme.colors.darkBackground, backgroundColor: currentTheme.colors.darkBackground,
}, },

View file

@ -287,7 +287,14 @@ const SearchScreen = () => {
setShowRecent(true); setShowRecent(true);
loadRecentSearches(); loadRecentSearches();
} else { } 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; const headerHeight = headerBaseHeight + topSpacing + 60;
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <Animated.View
style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
entering={Platform.OS === 'android' ? SlideInRight.duration(250) : FadeIn.duration(350)}
exiting={Platform.OS === 'android' ?
FadeOut.duration(200).withInitialValues({ opacity: 1 }) :
FadeOut.duration(250)
}
>
<StatusBar <StatusBar
barStyle="light-content" barStyle="light-content"
backgroundColor="transparent" backgroundColor="transparent"
@ -656,7 +670,7 @@ const SearchScreen = () => {
)} )}
</View> </View>
</View> </View>
</View> </Animated.View>
); );
}; };

View file

@ -33,6 +33,7 @@ import { useSettings } from '../hooks/useSettings';
import QualityBadge from '../components/metadata/QualityBadge'; import QualityBadge from '../components/metadata/QualityBadge';
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut,
FadeInDown, FadeInDown,
SlideInDown, SlideInDown,
withSpring, 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'); const { width, height } = Dimensions.get('window');
// Extracted Components // Extracted Components
const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme }: { const StreamCard = ({ stream, onPress, index, isLoading, statusMessage, theme, isExiting }: {
stream: Stream; stream: Stream;
onPress: () => void; onPress: () => void;
index: number; index: number;
isLoading?: boolean; isLoading?: boolean;
statusMessage?: string; statusMessage?: string;
theme: any; theme: any;
isExiting?: boolean;
}) => { }) => {
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); 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 displayTitle = isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream');
const displayAddonName = isHDRezka ? '' : (stream.title || ''); const displayAddonName = isHDRezka ? '' : (stream.title || '');
// Animation delay based on index - stagger effect // Animation delay based on index - stagger effect (only if not exiting)
const enterDelay = 100 + (index * 50); const enterDelay = isExiting ? 0 : 100 + (index * 30);
// Use simple View when exiting to prevent animation conflicts
if (isExiting) {
return (
<View>
<TouchableOpacity
style={[
styles.streamCard,
isLoading && styles.streamCardLoading
]}
onPress={onPress}
disabled={isLoading}
activeOpacity={0.7}
>
<View style={styles.streamDetails}>
<View style={styles.streamNameRow}>
<View style={styles.streamTitleContainer}>
<Text style={[styles.streamName, { color: theme.colors.highEmphasis }]}>
{displayTitle}
</Text>
{displayAddonName && displayAddonName !== displayTitle && (
<Text style={[styles.streamAddonName, { color: theme.colors.mediumEmphasis }]}>
{displayAddonName}
</Text>
)}
</View>
{/* Show loading indicator if stream is loading */}
{isLoading && (
<View style={styles.loadingIndicator}>
<ActivityIndicator size="small" color={theme.colors.primary} />
<Text style={[styles.loadingText, { color: theme.colors.primary }]}>
{statusMessage || "Loading..."}
</Text>
</View>
)}
</View>
<View style={styles.streamMetaRow}>
{quality && quality >= "720" && (
<QualityBadge type="HD" />
)}
{isDolby && (
<QualityBadge type="VISION" />
)}
{size && (
<View style={[styles.chip, { backgroundColor: theme.colors.darkGray }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>{size}</Text>
</View>
)}
{isDebrid && (
<View style={[styles.chip, { backgroundColor: theme.colors.success }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>DEBRID</Text>
</View>
)}
{/* Special badge for HDRezka streams */}
{isHDRezka && (
<View style={[styles.chip, { backgroundColor: theme.colors.accent }]}>
<Text style={[styles.chipText, { color: theme.colors.white }]}>HDREZKA</Text>
</View>
)}
</View>
</View>
<View style={styles.streamAction}>
<MaterialIcons
name="play-arrow"
size={24}
color={theme.colors.primary}
/>
</View>
</TouchableOpacity>
</View>
);
}
return ( return (
<Animated.View <Animated.View
entering={FadeInDown.duration(300).delay(enterDelay).springify()} entering={FadeInDown.duration(200).delay(enterDelay)}
layout={Layout.springify()} layout={Layout.duration(200)}
> >
<TouchableOpacity <TouchableOpacity
style={[ style={[
@ -249,6 +330,9 @@ export const StreamsScreen = () => {
// Add state for handling orientation transition // Add state for handling orientation transition
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
// Add state to prevent animation conflicts during exit
const [isExiting, setIsExiting] = useState(false);
// Add timing logs // Add timing logs
const [loadStartTime, setLoadStartTime] = useState(0); const [loadStartTime, setLoadStartTime] = useState(0);
@ -400,20 +484,25 @@ export const StreamsScreen = () => {
// Memoize handlers // Memoize handlers
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
// Set exit state to prevent animation conflicts and hide content immediately
setIsExiting(true);
const cleanup = () => { const cleanup = () => {
headerOpacity.value = withTiming(0, { duration: 200 }); headerOpacity.value = withTiming(0, { duration: 100 });
heroScale.value = withTiming(0.95, { duration: 200 }); heroScale.value = withTiming(0.95, { duration: 100 });
filterOpacity.value = withTiming(0, { duration: 200 }); filterOpacity.value = withTiming(0, { duration: 100 });
}; };
cleanup(); cleanup();
// For series episodes, always replace current screen with metadata screen // For series episodes, always replace current screen with metadata screen
if (type === 'series') { if (type === 'series') {
// Immediate navigation for series
navigation.replace('Metadata', { navigation.replace('Metadata', {
id: id, id: id,
type: type type: type
}); });
} else { } else {
// Immediate navigation for movies
navigation.goBack(); navigation.goBack();
} }
}, [navigation, headerOpacity, heroScale, filterOpacity, type, id]); }, [navigation, headerOpacity, heroScale, filterOpacity, type, id]);
@ -954,9 +1043,10 @@ export const StreamsScreen = () => {
isLoading={isLoading} isLoading={isLoading}
statusMessage={undefined} statusMessage={undefined}
theme={currentTheme} theme={currentTheme}
isExiting={isExiting}
/> />
); );
}, [handleStreamPress, currentTheme]); }, [handleStreamPress, currentTheme, isExiting]);
const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => { const renderSectionHeader = useCallback(({ section }: { section: { title: string; addonId: string } }) => {
const isProviderLoading = loadingProviders[section.addonId]; const isProviderLoading = loadingProviders[section.addonId];
@ -1035,6 +1125,11 @@ export const StreamsScreen = () => {
barStyle="light-content" barStyle="light-content"
/> />
{/* Instant overlay when exiting to prevent glitches */}
{isExiting && (
<View style={[StyleSheet.absoluteFill, { backgroundColor: colors.darkBackground, zIndex: 100 }]} />
)}
{/* Transition overlay to mask orientation changes */} {/* Transition overlay to mask orientation changes */}
{isTransitioning && ( {isTransitioning && (
<View style={styles.transitionOverlay}> <View style={styles.transitionOverlay}>