React-Native:位置更新时TouchableOpacity无法响应点击问题
React Native:位置更新时FlatList的TouchableOpacity无法响应点击问题
代码功能
实现罗盘/箭头旋转并指向指定坐标的功能,通过Expo Location获取用户位置和航向,计算目标坐标的方位角并驱动箭头动画旋转。
遇到的问题
当用户移动导致位置更新时,FlatList中Item组件的TouchableOpacity完全无法响应点击操作。
已尝试的方案
- 使用React.memo包裹Item组件,限制仅在props变化时重渲染,问题未解决
- 将
onPress替换为onPressIn,点击响应略有改善但仍未彻底解决
问题排查与修复方案
1. 核心原因:视图层级遮挡
你的Animated.Image设置了flex:1,会占据整个父容器空间,且默认位于FlatList上层,导致FlatList的触摸事件被Image拦截,TouchableOpacity根本接收不到点击信号。
修复:
调整Image和FlatList的布局,避免遮挡:
const styles = StyleSheet.create({ background: { backgroundColor: COLORS.background_Pale, flex: 1, }, image: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 80, // 为FlatList预留底部空间 resizeMode: 'contain', // 保持图片比例,避免拉伸 }, flatList: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, // 固定FlatList高度 }, item: { padding: 20, marginVertical: 8, marginHorizontal: 16, borderRadius: 8, }, title: { fontSize: 16, }, });
2. 修复useEffect依赖缺失
第二个useEffect的依赖数组仅包含userHeading,但内部用到了userLocation和angle,会导致闭包捕获旧状态值,且位置更新时无法触发方位角计算。
修复:
补充完整依赖项:
useEffect(() => { const rotateImage = (angle) => { Animated.timing(rotation, { toValue: angle, duration: 300, easing: Easing.linear, useNativeDriver: true, }).start(); }; const getBearing = () => { const bearing = geolib.getGreatCircleBearing( { latitude: userLocation.coords.latitude, longitude: userLocation.coords.longitude, }, { latitude: 45.472748, longitude: -73.862076, } ); return bearing; }; const checkHeading = setTimeout(() => { if (userLocation && userHeading !== null) { // 增加userHeading非空判断 let newAngle = getBearing() - userHeading; let delta = newAngle - angle; while (delta > 180 || delta < -180) { if (delta > 180) { newAngle -= 360; } else if (delta < -180) { newAngle += 360; } delta = newAngle - angle; } if (Math.abs(delta) > 5) { // 简化判断 setAngle(newAngle); rotateImage(newAngle); } } }, 0); return () => clearTimeout(checkHeading); }, [userHeading, userLocation, angle]); // 补充缺失的依赖
3. 优化回调函数,避免不必要重渲染
renderItem中每次都会创建新的onPress回调,导致即使Item用了memo也会频繁重渲染,同时改回onPress(onPressIn不是正确解决方案)。
修复:
使用useCallback缓存回调函数,并用memo包裹Item:
// 缓存点击回调 const handleItemPress = useCallback((itemId) => { setSelectedId(itemId); console.warn("bob"); }, []); // 用memo包裹Item组件 const Item = memo(({ item, onPress, backgroundColor, textColor }) => ( <TouchableOpacity onPress={onPress} style={[styles.item, { backgroundColor }]} > <Text style={[styles.title, { color: textColor }]}>{item.title}</Text> </TouchableOpacity> )); const renderItem = ({ item }) => { const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff"; const color = item.id === selectedId ? "white" : "black"; return ( <Item item={item} onPress={() => handleItemPress(item.id)} backgroundColor={backgroundColor} textColor={color} /> ); };
4. 降低位置更新频率
watchPositionAsync设置distanceInterval:0会导致位置微小变化就触发更新,增加组件渲染压力,设置合理阈值:
const location = await Location.watchPositionAsync( { accuracy: Location.Accuracy.BestForNavigation, distanceInterval: 1, // 移动1米才更新位置 }, (location) => { setUserLocation(location); } );
完整修复后代码
import { Alert, Animated, Easing, FlatList, Linking, Platform, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import React, { useEffect, memo, useRef, useState, useCallback } from "react"; import * as Location from "expo-location"; import * as geolib from "geolib"; import RNAndroidOpenSettings from "react-native-android-open-settings"; import { COLORS } from "../../assets/Colors/Colors"; export default function DateFinder() { const [hasForegroundPermissions, setHasForegroundPermissions] = useState(null); const [userLocation, setUserLocation] = useState(null); const [userHeading, setUserHeading] = useState(null); const [angle, setAngle] = useState(0); const rotation = useRef(new Animated.Value(0)).current; const [selectedId, setSelectedId] = useState(); useEffect(() => { const AccessLocation = async () => { function appSettings() { console.warn("Open settings pressed"); if (Platform.OS === "ios") { Linking.openURL("app-settings:"); } else RNAndroidOpenSettings.appDetailsSettings(); } const appSettingsAlert = () => { Alert.alert( "Allow Wassupp to Use your Location", "Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass", [ { text: "Cancel", onPress: () => console.warn("Cancel pressed"), }, { text: "Open settings", onPress: appSettings }, ] ); }; const foregroundPermissions = await Location.requestForegroundPermissionsAsync(); if ( foregroundPermissions.canAskAgain === false || foregroundPermissions.status === "denied" ) { appSettingsAlert(); } setHasForegroundPermissions(foregroundPermissions.status === "granted"); if (foregroundPermissions.status === "granted") { const location = await Location.watchPositionAsync( { accuracy: Location.Accuracy.BestForNavigation, distanceInterval: 1, }, (location) => { setUserLocation(location); } ); const heading = await Location.watchHeadingAsync((heading) => { setUserHeading(heading.trueHeading); }); return () => { location.remove(); heading.remove(); }; } }; AccessLocation().catch(console.error); }, []); useEffect(() => { const rotateImage = (angle) => { Animated.timing(rotation, { toValue: angle, duration: 300, easing: Easing.linear, useNativeDriver: true, }).start(); }; const getBearing = () => { const bearing = geolib.getGreatCircleBearing( { latitude: userLocation.coords.latitude, longitude: userLocation.coords.longitude, }, { latitude: 45.472748, longitude: -73.862076, } ); return bearing; }; const checkHeading = setTimeout(() => { if (userLocation && userHeading !== null) { let newAngle = getBearing() - userHeading; let delta = newAngle - angle; while (delta > 180 || delta < -180) { if (delta > 180) { newAngle -= 360; } else if (delta < -180) { newAngle += 360; } delta = newAngle - angle; } if (Math.abs(delta) > 5) { setAngle(newAngle); rotateImage(newAngle); } } }, 0); return () => clearTimeout(checkHeading); }, [userHeading, userLocation, angle]); const textPosition = userLocation ? JSON.stringify(userLocation) : "Loading location..."; const DATA = [ { id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba", title: "First Item", }, { id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63", title: "Second Item", }, { id: "58694a0f-3da1-471f-bd96-145571e29d72", title: "Third Item", }, ]; const handleItemPress = useCallback((itemId) => { setSelectedId(itemId); console.warn("bob"); }, []); const Item = memo(({ item, onPress, backgroundColor, textColor }) => ( <TouchableOpacity onPress={onPress} style={[styles.item, { backgroundColor }]} > <Text style={[styles.title, { color: textColor }]}>{item.title}</Text> </TouchableOpacity> )); const renderItem = ({ item }) => { const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff"; const color = item.id === selectedId ? "white" : "black"; return ( <Item item={item} onPress={() => handleItemPress(item.id)} backgroundColor={backgroundColor} textColor={color} /> ); }; return ( <View style={styles.background}> <Text>{textPosition}</Text> <Animated.Image source={require("../../assets/Compass/Arrow_up.png")} style={[ styles.image, { transform: [ { rotate: rotation.interpolate({ inputRange: [0, 360], outputRange: ["0deg", "360deg"], }), }, ], }, ]} /> <FlatList data={DATA} extraData={selectedId} horizontal={true} keyExtractor={(item) => item.id} renderItem={renderItem} style={styles.flatList} /> </View> ); } const styles = StyleSheet.create({ background: { backgroundColor: COLORS.background_Pale, flex: 1, }, image: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 80, resizeMode: 'contain', }, flatList: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 80, backgroundColor: COLORS.background_Pale, }, item: { padding: 20, marginVertical: 8, marginHorizontal: 16, borderRadius: 8, }, title: { fontSize: 16, }, });
内容的提问来源于stack exchange,提问作者Rémi-Antoine Joron
相关产品推荐
相关产品推荐

