From 698456c205bdc9fde2e728c4877de56b43d716ba Mon Sep 17 00:00:00 2001 From: Qarqun Date: Sun, 19 Oct 2025 01:11:08 +0800 Subject: [PATCH 1/2] refactor(player): enhance video controls with modern streaming style - Moved inline styles to playerStyles.ts for better maintainability - Redesigned player controls for better user experience: - Enhanced skip buttons with rotate animations and semi-transparent backgrounds - Enlarged center play/pause button with improved visibility - Optimized touch targets and spacing for better interaction - Standardized button dimensions and layout Files changed: - src/components/player/controls/PlayerControls.tsx - src/components/player/utils/playerStyles.ts --- .vscode/settings.json | 1 + .../exoplayer/ReactExoplayerView.java | 2 +- .../player/controls/PlayerControls.tsx | 20 ++++++++++++++++--- src/components/player/utils/playerStyles.ts | 20 ++++++++++++++----- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41..7b016a8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,3 @@ { + "java.compile.nullAnalysis.mode": "automatic" } \ No newline at end of file diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 87e436a..f175dec 100644 --- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -726,7 +726,7 @@ public class ReactExoplayerView extends FrameLayout implements DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext()) - .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) + .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF) .setEnableDecoderFallback(true) .forceEnableMediaCodecAsynchronousQueueing(); diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index 44a92e5..0588d76 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -148,15 +148,29 @@ export const PlayerControls: React.FC = ({ {/* Center Controls (Play/Pause, Skip) */} + {/* Left Skip Button */} skip(-10)} style={styles.skipButton}> - + + + 10 + + {/* Play/Pause Button */} - + + + {/* Right Skip Button */} skip(10)} style={styles.skipButton}> - + + + 10 diff --git a/src/components/player/utils/playerStyles.ts b/src/components/player/utils/playerStyles.ts index 6b7b8cc..d5de3f3 100644 --- a/src/components/player/utils/playerStyles.ts +++ b/src/components/player/utils/playerStyles.ts @@ -100,28 +100,38 @@ export const styles = StyleSheet.create({ controls: { position: 'absolute', flexDirection: 'row', - justifyContent: 'center', + justifyContent: 'space-between', alignItems: 'center', - gap: 40, left: 0, right: 0, top: '50%', transform: [{ translateY: -30 }], + paddingHorizontal: 40, zIndex: 1000, }, playButton: { - justifyContent: 'center', alignItems: 'center', - padding: 10, + justifyContent: 'center', + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 40, + padding: 15, + width: 80, + height: 80, }, skipButton: { + flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.5)', + borderRadius: 8, + padding: 12, + width: 60, }, skipText: { color: 'white', fontSize: 12, - marginTop: 2, + fontWeight: '600', + marginTop: 4, }, bottomControls: { gap: 12, -- 2.45.2 From e305dee777a465539c995111fe0bea056d56a565 Mon Sep 17 00:00:00 2001 From: Qarqun Date: Mon, 20 Oct 2025 19:13:46 +0800 Subject: [PATCH 2/2] feat: Add responsive video player controls with animations - Implement responsive sizing for all controls based on screen width (phone/tablet support) - Add smooth crossfade animation for play/pause button transitions - Add arc sweep and slide animations for seek buttons (+10/-10s) - Add touch feedback with semi-transparent circle flash on tap --- .../player/controls/PlayerControls.tsx | 345 ++++++++++++++++-- src/components/player/utils/playerStyles.ts | 98 ++++- 2 files changed, 402 insertions(+), 41 deletions(-) diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index 0588d76..420aa37 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform } from 'react-native'; +import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform, Dimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import Slider from '@react-native-community/slider'; @@ -82,6 +82,156 @@ export const PlayerControls: React.FC = ({ playerBackend, }) => { const { currentTheme } = useTheme(); + + + /* Responsive Spacing */ + const screenWidth = Dimensions.get('window').width; + const buttonSpacing = screenWidth * 0.15; + + const playButtonSize = screenWidth * 0.12; // 12% of screen width + const playIconSize = playButtonSize * 0.6; // 60% of button size + const seekButtonSize = screenWidth * 0.11; // 11% of screen width + const seekIconSize = seekButtonSize * 0.75; // 75% of button size + const seekNumberSize = seekButtonSize * 0.25; // 25% of button size + const arcBorderWidth = seekButtonSize * 0.05; // 5% of button size + + /* Animations - State & Refs */ + const [showBackwardSign, setShowBackwardSign] = React.useState(false); + const [showForwardSign, setShowForwardSign] = React.useState(false); + + /* Separate Animations for Each Button */ + const backwardPressAnim = React.useRef(new Animated.Value(0)).current; + const backwardSlideAnim = React.useRef(new Animated.Value(0)).current; + const backwardScaleAnim = React.useRef(new Animated.Value(1)).current; + const backwardArcOpacity = React.useRef(new Animated.Value(0)).current; + const backwardArcRotation = React.useRef(new Animated.Value(0)).current; + + const forwardPressAnim = React.useRef(new Animated.Value(0)).current; + const forwardSlideAnim = React.useRef(new Animated.Value(0)).current; + const forwardScaleAnim = React.useRef(new Animated.Value(1)).current; + const forwardArcOpacity = React.useRef(new Animated.Value(0)).current; + const forwardArcRotation = React.useRef(new Animated.Value(0)).current; + + const playPressAnim = React.useRef(new Animated.Value(0)).current; + const playIconScale = React.useRef(new Animated.Value(1)).current; + const playIconOpacity = React.useRef(new Animated.Value(1)).current; + + /* Handle Seek with Animation */ + const handleSeekWithAnimation = (seconds: number) => { + const isForward = seconds > 0; + + if (isForward) { + setShowForwardSign(true); + } else { + setShowBackwardSign(true); + } + + const pressAnim = isForward ? forwardPressAnim : backwardPressAnim; + const slideAnim = isForward ? forwardSlideAnim : backwardSlideAnim; + const scaleAnim = isForward ? forwardScaleAnim : backwardScaleAnim; + const arcOpacity = isForward ? forwardArcOpacity : backwardArcOpacity; + const arcRotation = isForward ? forwardArcRotation : backwardArcRotation; + + Animated.parallel([ + // Button press effect (circle flash) + Animated.sequence([ + Animated.timing(pressAnim, { + toValue: 1, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(pressAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]), + // Number slide out + Animated.sequence([ + Animated.timing(slideAnim, { + toValue: isForward ? (seekButtonSize * 0.75) : -(seekButtonSize * 0.75), + duration: 250, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: 0, + duration: 120, + useNativeDriver: true, + }), + ]), + // Button scale pulse + Animated.sequence([ + Animated.timing(scaleAnim, { + toValue: 1.15, + duration: 150, + useNativeDriver: true, + }), + Animated.timing(scaleAnim, { + toValue: 1, + duration: 150, + useNativeDriver: true, + }), + ]), + // Arc sweep animation + Animated.parallel([ + Animated.timing(arcOpacity, { + toValue: 1, + duration: 50, + useNativeDriver: true, + }), + Animated.timing(arcRotation, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + ]), + ]).start(() => { + if (isForward) { + setShowForwardSign(false); + } else { + setShowBackwardSign(false); + } + arcOpacity.setValue(0); + arcRotation.setValue(0); + }); + + skip(seconds); + }; + + /* Handle Play/Pause with Animation */ + const handlePlayPauseWithAnimation = () => { + Animated.sequence([ + Animated.timing(playPressAnim, { + toValue: 1, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(playPressAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + + Animated.sequence([ + Animated.timing(playIconScale, { + toValue: 0.85, + duration: 150, + useNativeDriver: true, + }), + Animated.timing(playIconScale, { + toValue: 1, + duration: 150, + useNativeDriver: true, + }), + ]).start(); + + togglePlayback(); + }; + + + + return ( = ({ - {/* Center Controls (Play/Pause, Skip) */} - - {/* Left Skip Button */} - skip(-10)} style={styles.skipButton}> - - - - 10 + + {/* Center Controls - CloudStream Style */} + + + {/* Backward Seek Button (-10s) */} + handleSeekWithAnimation(-10)} + activeOpacity={0.7} + > + + + + + + {showBackwardSign ? '-10' : '10'} + + + + + + {/* Play/Pause Button */} - - + + + + + + + - {/* Right Skip Button */} - skip(10)} style={styles.skipButton}> - - - - 10 + {/* Forward Seek Button (+10s) */} + handleSeekWithAnimation(10)} + activeOpacity={0.7} + > + + + + + + {showForwardSign ? '+10' : '10'} + + + + + + + + + + {/* Bottom Gradient */}