You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 09:57:03