mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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:
parent
026c425660
commit
93a016b96e
3 changed files with 149 additions and 16 deletions
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue