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}
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,
},

View file

@ -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 (
<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
barStyle="light-content"
backgroundColor="transparent"
@ -656,7 +670,7 @@ const SearchScreen = () => {
)}
</View>
</View>
</View>
</Animated.View>
);
};

View file

@ -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 (
<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 (
<Animated.View
entering={FadeInDown.duration(300).delay(enterDelay).springify()}
layout={Layout.springify()}
entering={FadeInDown.duration(200).delay(enterDelay)}
layout={Layout.duration(200)}
>
<TouchableOpacity
style={[
@ -249,6 +330,9 @@ export const StreamsScreen = () => {
// 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 && (
<View style={[StyleSheet.absoluteFill, { backgroundColor: colors.darkBackground, zIndex: 100 }]} />
)}
{/* Transition overlay to mask orientation changes */}
{isTransitioning && (
<View style={styles.transitionOverlay}>