From 68b8b7d665718346bceabfb593510e308ae23a2f Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 25 Mar 2026 13:36:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BD=BB=E5=BA=95=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=B0=8F=E7=AA=97=E6=97=A0=E6=B3=95=E6=8B=96=E5=8A=A8=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根本原因:TouchableOpacity 内嵌在 PanResponder Animated.View 内, JS层 TouchableOpacity 与 Video 原生层共同阻断了 PanResponder 响应权。 修复方案: - onStartShouldSetPanResponder: true,从 start 阶段独占响应权 - 移除主内容区的 TouchableOpacity,改用纯 View - isDragging ref 区分点击与拖动:位移 >5px 为拖动 - onPanResponderRelease 通过 locationX/Y 坐标判断点击目标(关闭 or 跳转) - Video 外层 View 加 pointerEvents='none' 防止原生层吞噬触摸 --- components/LiveMiniPlayer.tsx | 144 +++++++++++++++++----------------- components/MiniPlayer.tsx | 60 +++++++------- 2 files changed, 104 insertions(+), 100 deletions(-) diff --git a/components/LiveMiniPlayer.tsx b/components/LiveMiniPlayer.tsx index 4237f30..6b73cb8 100644 --- a/components/LiveMiniPlayer.tsx +++ b/components/LiveMiniPlayer.tsx @@ -4,7 +4,6 @@ import { Text, Image, StyleSheet, - TouchableOpacity, Animated, PanResponder, Dimensions, @@ -26,84 +25,86 @@ const LIVE_HEADERS = { 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', }; +function snapRelease( + pan: Animated.ValueXY, + curX: number, + curY: number, + sw: number, + sh: number, +) { + const snapRight = 0; + const snapLeft = -(sw - MINI_W - 24); + const snapX = curX < snapLeft / 2 ? snapLeft : snapRight; + const clampedY = Math.max(-sh + MINI_H + 60, Math.min(60, curY)); + Animated.spring(pan, { + toValue: { x: snapX, y: clampedY }, + useNativeDriver: false, + tension: 120, + friction: 10, + }).start(); +} + export function LiveMiniPlayer() { const { isActive, roomId, title, cover, hlsUrl, clearLive } = useLiveStore(); const videoMiniActive = useVideoStore(s => s.isActive); const router = useRouter(); const insets = useSafeAreaInsets(); const pan = useRef(new Animated.ValueXY()).current; + const isDragging = useRef(false); const panResponder = useRef( PanResponder.create({ - // 不在 start 阶段抢夺,让 TouchableOpacity 的点击正常触发 - onStartShouldSetPanResponder: () => false, - onStartShouldSetPanResponderCapture: () => false, - // 有实际位移时,从子组件夺回响应权(capture 阶段,优先于子组件) - onMoveShouldSetPanResponder: (_, { dx, dy }) => - Math.abs(dx) > 3 || Math.abs(dy) > 3, - onMoveShouldSetPanResponderCapture: (_, { dx, dy }) => - Math.abs(dx) > 3 || Math.abs(dy) > 3, + onStartShouldSetPanResponder: () => true, onPanResponderGrant: () => { + isDragging.current = false; pan.setOffset({ x: (pan.x as any)._value, y: (pan.y as any)._value }); pan.setValue({ x: 0, y: 0 }); }, - onPanResponderMove: Animated.event( - [null, { dx: pan.x, dy: pan.y }], - { useNativeDriver: false }, - ), - onPanResponderRelease: () => { + onPanResponderMove: (_, gs) => { + if (Math.abs(gs.dx) > 5 || Math.abs(gs.dy) > 5) { + isDragging.current = true; + } + pan.x.setValue(gs.dx); + pan.y.setValue(gs.dy); + }, + onPanResponderRelease: (evt) => { pan.flattenOffset(); + if (!isDragging.current) { + const { locationX, locationY } = evt.nativeEvent; + if (locationX > MINI_W - 28 && locationY < 28) { + clearLive(); + } else { + router.push(`/live/${roomId}` as any); + } + return; + } const { width: sw, height: sh } = Dimensions.get('window'); - const curX = (pan.x as any)._value; - const curY = (pan.y as any)._value; - - // 吸附到左边缘或右边缘(取最近的一侧) - const snapRight = 0; - const snapLeft = -(sw - MINI_W - 24); - const snapX = curX < snapLeft / 2 ? snapLeft : snapRight; - - const clampedY = Math.max(-sh + MINI_H + 60, Math.min(60, curY)); - - Animated.spring(pan, { - toValue: { x: snapX, y: clampedY }, - useNativeDriver: false, - tension: 120, - friction: 10, - }).start(); - }, - onPanResponderTerminate: () => { - pan.flattenOffset(); + snapRelease(pan, (pan.x as any)._value, (pan.y as any)._value, sw, sh); }, + onPanResponderTerminate: () => { pan.flattenOffset(); }, }), ).current; if (!isActive) return null; - // 视频 MiniPlayer 激活时,直播小窗叠放其上方(避免重叠) const bottomOffset = insets.bottom + 16 + (videoMiniActive ? 106 : 0); - const handlePress = () => { - router.push(`/live/${roomId}` as any); - }; - - // Web 端降级:展示封面图 + LIVE 徽标 + // Web 端降级:封面图 + LIVE 徽标 if (Platform.OS === 'web') { return ( - - - - - LIVE - - {title} - - + + + + LIVE + + {title} + - + ); } @@ -116,29 +117,29 @@ export function LiveMiniPlayer() { style={[styles.container, { bottom: bottomOffset, transform: pan.getTranslateTransform() }]} {...panResponder.panHandlers} > - - - - - - LIVE - - {title} - - + {/* pointerEvents="none" 防止 Video 原生层吞噬触摸事件 */} + + + + + LIVE + + {title} + {/* 关闭按钮视觉层,点击逻辑由 onPanResponderRelease 坐标判断 */} + - + ); } @@ -158,7 +159,6 @@ const styles = StyleSheet.create({ shadowOpacity: 0.3, shadowRadius: 4, }, - main: { flex: 1 }, videoArea: { width: '100%', height: 66, diff --git a/components/MiniPlayer.tsx b/components/MiniPlayer.tsx index 0d01f56..84817d8 100644 --- a/components/MiniPlayer.tsx +++ b/components/MiniPlayer.tsx @@ -1,6 +1,6 @@ import React, { useRef } from 'react'; import { - View, Text, Image, StyleSheet, TouchableOpacity, + View, Text, Image, StyleSheet, Animated, PanResponder, Dimensions, } from 'react-native'; import { useRouter } from 'expo-router'; @@ -17,32 +17,44 @@ export function MiniPlayer() { const router = useRouter(); const insets = useSafeAreaInsets(); const pan = useRef(new Animated.ValueXY()).current; + const isDragging = useRef(false); const panResponder = useRef( PanResponder.create({ - onStartShouldSetPanResponder: () => false, - onStartShouldSetPanResponderCapture: () => false, - onMoveShouldSetPanResponder: (_, { dx, dy }) => - Math.abs(dx) > 3 || Math.abs(dy) > 3, - onMoveShouldSetPanResponderCapture: (_, { dx, dy }) => - Math.abs(dx) > 3 || Math.abs(dy) > 3, + // 从 start 阶段即抢占响应权,确保拖动可靠触发 + onStartShouldSetPanResponder: () => true, onPanResponderGrant: () => { + isDragging.current = false; pan.setOffset({ x: (pan.x as any)._value, y: (pan.y as any)._value }); pan.setValue({ x: 0, y: 0 }); }, - onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false }), - onPanResponderRelease: () => { + onPanResponderMove: (_, gs) => { + if (Math.abs(gs.dx) > 5 || Math.abs(gs.dy) > 5) { + isDragging.current = true; + } + pan.x.setValue(gs.dx); + pan.y.setValue(gs.dy); + }, + onPanResponderRelease: (evt) => { pan.flattenOffset(); + if (!isDragging.current) { + // 点击:通过坐标判断是关闭还是跳转 + const { locationX, locationY } = evt.nativeEvent; + if (locationX > MINI_W - 28 && locationY < 28) { + clearVideo(); + } else { + router.push(`/video/${bvid}` as any); + } + return; + } + // 拖动:吸附到最近边缘 const { width: sw, height: sh } = Dimensions.get('window'); const curX = (pan.x as any)._value; const curY = (pan.y as any)._value; - const snapRight = 0; const snapLeft = -(sw - MINI_W - 24); const snapX = curX < snapLeft / 2 ? snapLeft : snapRight; - const clampedY = Math.max(-sh + MINI_H + 60, Math.min(60, curY)); - Animated.spring(pan, { toValue: { x: snapX, y: clampedY }, useNativeDriver: false, @@ -50,9 +62,7 @@ export function MiniPlayer() { friction: 10, }).start(); }, - onPanResponderTerminate: () => { - pan.flattenOffset(); - }, + onPanResponderTerminate: () => { pan.flattenOffset(); }, }) ).current; @@ -65,17 +75,12 @@ export function MiniPlayer() { style={[styles.container, { bottom: bottomOffset, transform: pan.getTranslateTransform() }]} {...panResponder.panHandlers} > - router.push(`/video/${bvid}` as any)} - activeOpacity={0.85} - > - - {title} - - + + {title} + {/* 关闭按钮仅作视觉展示,点击逻辑由 onPanResponderRelease 坐标判断处理 */} + - + ); } @@ -84,8 +89,8 @@ const styles = StyleSheet.create({ container: { position: 'absolute', right: 12, - width: 160, - height: 90, + width: MINI_W, + height: MINI_H, borderRadius: 8, backgroundColor: '#1a1a1a', overflow: 'hidden', @@ -95,7 +100,6 @@ const styles = StyleSheet.create({ shadowOpacity: 0.3, shadowRadius: 4, }, - main: { flex: 1 }, cover: { width: '100%', height: 64, backgroundColor: '#333' }, title: { color: '#fff',