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