我正在开发一个 React Native 应用程序,其中包含可以平移和居中的气泡网格。我正在使用react-native-gesture-handler和react-native-reanimated,但我在居中逻辑方面遇到了问题。
平移后,我尝试选择最接近屏幕中心的气泡,但我的 findMostCenteredBubble 函数未按预期工作。
findMostCenteredBubble 函数选择视觉上远离中心的气泡,而不是屏幕上最中心的气泡。
import React, {useState, useRef, useEffect} from 'react';
import {
View,
TouchableOpacity,
Text,
StyleSheet,
Dimensions,
} from 'react-native';
import Animated, {
ZoomIn,
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS,
} from 'react-native-reanimated';
import {
GestureHandlerRootView,
GestureDetector,
Gesture,
} from 'react-native-gesture-handler';
const bubbles = Array.from({length: 60}, (_, i) => ({
id: i + 1,
label: `Bubble ${i + 1}`,
}));
interface BubblePosition {
id: number;
x: number;
y: number;
}
const BUBBLE_SIZE = 80;
const BUBBLE_MARGIN = 10;
const BUBBLES_PER_ROW = 6;
const {width: SCREEN_WIDTH, height: SCREEN_HEIGHT} = Dimensions.get('window');
const BubbleEffect = () => {
const [selectedBubble, setSelectedBubble] = useState<number | null>(null);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const offsetX = useSharedValue(0);
const offsetY = useSharedValue(0);
const totalWidth = BUBBLES_PER_ROW * (BUBBLE_SIZE + BUBBLE_MARGIN * 2);
const totalHeight =
Math.ceil(bubbles.length / BUBBLES_PER_ROW) *
(BUBBLE_SIZE + BUBBLE_MARGIN * 2);
const initialOffsetX = (SCREEN_WIDTH - totalWidth) / 2;
const initialOffsetY = (SCREEN_HEIGHT - totalHeight) / 2;
console.log('Screen Dimensions:', {SCREEN_WIDTH, SCREEN_HEIGHT});
console.log('Grid Dimensions:', {totalWidth, totalHeight});
console.log('Initial Offset:', {initialOffsetX, initialOffsetY});
const bubblePositions: BubblePosition[] = bubbles.map((_, index) => ({
id: index + 1,
x: (index % BUBBLES_PER_ROW) * (BUBBLE_SIZE + BUBBLE_MARGIN * 2),
y: Math.floor(index / BUBBLES_PER_ROW) * (BUBBLE_SIZE + BUBBLE_MARGIN * 2),
}));
const handleBubblePress = (id: number) => {
setSelectedBubble(id);
centerBubble(id);
};
const findMostCenteredBubble = () => {
'worklet';
const centerX = SCREEN_WIDTH / 2;
const centerY = SCREEN_HEIGHT / 2;
console.log('Screen Center:', {centerX, centerY});
console.log('Current Translation:', {
x: translateX.value,
y: translateY.value,
});
return bubblePositions.reduce(
(closest, bubble) => {
const bubbleCenterX =
bubble.x + translateX.value + BUBBLE_SIZE / 2 + initialOffsetX;
const bubbleCenterY =
bubble.y + translateY.value + BUBBLE_SIZE / 2 + initialOffsetY;
const distance = Math.sqrt(
Math.pow(bubbleCenterX - centerX, 2) +
Math.pow(bubbleCenterY - centerY, 2),
);
console.log(`Bubble ${bubble.id}:`, {
centerX: bubbleCenterX,
centerY: bubbleCenterY,
distance,
});
if (distance < closest.distance) {
return {id: bubble.id, distance};
}
return closest;
},
{id: -1, distance: Infinity},
);
};
const centerBubble = (bubbleId: number) => {
'worklet';
const bubble = bubblePositions.find(b => b.id === bubbleId);
if (bubble) {
const targetX =
SCREEN_WIDTH / 2 - (bubble.x + BUBBLE_SIZE / 2 + initialOffsetX);
const targetY =
SCREEN_HEIGHT / 2 - (bubble.y + BUBBLE_SIZE / 2 + initialOffsetY);
console.log('Centering Bubble:', {id: bubbleId, targetX, targetY});
translateX.value = withSpring(targetX);
translateY.value = withSpring(targetY);
}
};
const panGesture = Gesture.Pan()
.onStart(() => {
offsetX.value = translateX.value;
offsetY.value = translateY.value;
})
.onUpdate(event => {
translateX.value = offsetX.value + event.translationX;
translateY.value = offsetY.value + event.translationY;
})
.onEnd(() => {
console.log('Pan Gesture Ended');
const mostCentered = findMostCenteredBubble();
if (mostCentered.id !== -1) {
console.log('Most Centered Bubble:', mostCentered);
centerBubble(mostCentered.id);
runOnJS(setSelectedBubble)(mostCentered.id);
}
});
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{translateX: translateX.value},
{translateY: translateY.value},
],
};
});
// Initial centering
useEffect(() => {
const initialCenterId = Math.ceil(bubbles.length / 2);
console.log('Initial Centering:', initialCenterId);
centerBubble(initialCenterId);
setSelectedBubble(initialCenterId);
}, []);
return (
<GestureHandlerRootView style={styles.safeArea}>
<GestureDetector gesture={panGesture}>
<Animated.View style={[styles.scrollContainer, animatedStyle]}>
<View
style={[
styles.container,
{
width: totalWidth,
height: totalHeight,
left: initialOffsetX,
top: initialOffsetY,
},
]}>
{bubblePositions.map(bubble => (
<TouchableOpacity
key={bubble.id}
onPress={() => handleBubblePress(bubble.id)}
activeOpacity={0.7}
style={[
styles.bubbleWrapper,
{
left: bubble.x,
top: bubble.y,
},
]}>
<Animated.View
entering={ZoomIn.duration(300)}
style={[
styles.bubble,
selectedBubble === bubble.id && styles.selectedBubble,
]}>
<Text style={styles.label}>
{bubbles[bubble.id - 1].label}
</Text>
</Animated.View>
</TouchableOpacity>
))}
</View>
</Animated.View>
</GestureDetector>
</GestureHandlerRootView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#fff',
},
scrollContainer: {
flex: 1,
},
container: {
position: 'absolute',
},
bubbleWrapper: {
position: 'absolute',
width: BUBBLE_SIZE + BUBBLE_MARGIN * 2,
height: BUBBLE_SIZE + BUBBLE_MARGIN * 2,
justifyContent: 'center',
alignItems: 'center',
},
bubble: {
width: BUBBLE_SIZE,
height: BUBBLE_SIZE,
borderRadius: BUBBLE_SIZE / 2,
backgroundColor: '#D3D3D3',
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 5,
shadowOffset: {width: 2, height: 2},
},
selectedBubble: {
backgroundColor: '#87CEEB',
},
label: {
color: '#000',
fontSize: 14,
fontWeight: 'bold',
},
});
export default BubbleEffect;
问题的出现是因为在 findMostCenteredBubble 函数中两次包含了initialOffsetX和initialOffsetY。它们已计入translateX 和translateY 中。 计算bubbleCenterX和bubbleCenterY时只需去掉initialOffsetX和initialOffsetY即可。