ReactNative项目OpenHarmony三方库集成实战:@react-native-community/geolocation

欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net

📋 前言

地理位置服务(Geolocation)是一项核心功能。无论是地图导航、外卖配送、社交签到,还是运动健身类应用,都需要获取用户的地理位置信息。@react-native-community/geolocation 是 React Native 官方社区维护的定位库,提供了统一的 API 接口,支持获取当前位置、持续监听位置变化等功能,是开发位置相关应用的基础组件。

🎯 库简介

基本信息

  • 库名称 : @react-native-community/geolocation
  • 版本信息 :
    • 3.1.1 + @react-native-ohos/geolocation: 支持 RN 0.72 版本
    • 3.4.1 + @react-native-ohos/geolocation: 支持 RN 0.77 版本
  • 官方仓库: https://github.com/react-native-oh-library/react-native-geolocation/tree/sig
  • 主要功能 :
    • 📍 获取当前设备位置
    • 🔄 持续监听位置变化
    • ⚙️ 配置定位参数
    • 🎯 支持精度控制
    • 📱 跨平台一致性表现

为什么选择 Geolocation?

特性 原生定位实现 @react-native-community/geolocation
跨平台统一API ❌ 需分别开发 ✅ 统一接口
权限管理 ⚠️ 需手动处理 ✅ 自动请求权限
位置监听 ⚠️ 需自行实现 ✅ 内置支持
配置灵活
HarmonyOS支持

支持的 API

API 说明 HarmonyOS 支持
setRNConfiguration 设置全局配置选项 ⚠️ 部分支持
requestAuthorization 请求位置权限 ⚠️ 部分支持
getCurrentPosition 获取当前位置 ⚠️ 部分支持
watchPosition 持续监听位置变化 ⚠️ 部分支持
clearWatch 清除位置监听
stopObserving 停止所有监听

兼容性验证

在以下环境验证通过:

  1. RNOH : 0.72.90; SDK : HarmonyOS 6.0.0 Release SDK; IDE : DevEco Studio 6.0.2; ROM: 6.0.0

📦 安装步骤

1. 安装依赖

在项目根目录执行以下命令,本文基于 RN 0.72.90 版本开发:

bash 复制代码
# RN 0.72 版本
npm install @react-native-ohos/geolocation@3.1.1-rc.1

# 或者使用 yarn
yarn add @react-native-ohos/geolocation@3.1.1-rc.1

2. 验证安装

安装完成后,检查 package.json 文件,应该能看到新增的依赖:

json 复制代码
{
  "dependencies": {
    "@react-native-ohos/geolocation": "3.1.1-rc.1",
    // ... 其他依赖
  }
}

🔧 HarmonyOS 平台配置 ⭐

由于 HarmonyOS 暂不支持 AutoLink(3.1.1 版本支持),需要手动配置原生端代码。本文采用HAR 包引入的方式。

1. 在工程根目录的 oh-package.json5 添加 overrides 字段

打开 harmony/oh-package.json5,添加以下配置:

json5 复制代码
{
  // ... 其他配置
  "overrides": {
    "@rnoh/react-native-openharmony": "0.72.90"
  }
}

2. 在 entry/oh-package.json5 添加依赖

打开 harmony/entry/oh-package.json5,添加以下依赖:

json5 复制代码
"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  "@react-native-ohos/geolocation": "file:../../node_modules/@react-native-ohos/geolocation/harmony/geolocation.har"
}

3. 同步依赖

点击 DevEco Studio 右上角的 sync 按钮,或者在终端执行:

bash 复制代码
cd harmony/entry
ohpm install

4. 配置 CMakeLists.txt

打开 harmony/entry/src/main/cpp/CMakeLists.txt,添加以下配置:

cmake 复制代码
project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")
set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../react-native-harmony/harmony/cpp")
set(LOG_VERBOSITY_LEVEL 1)
set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
set(WITH_HITRACE_SYSTRACE 1)
add_compile_definitions(WITH_HITRACE_SYSTRACE)

add_subdirectory("${RNOH_CPP_DIR}" ./rn)

# 添加 Geolocation 模块
+ set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/geolocation/src/main/cpp" ./geolocation)

file(GLOB GENERATED_CPP_FILES "./generated/*.cpp")

add_library(rnoh_app SHARED
    ${GENERATED_CPP_FILES}
    "./PackageProvider.cpp"
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
)
target_link_libraries(rnoh_app PUBLIC rnoh)

# 链接 Geolocation 库
+ target_link_libraries(rnoh_app PUBLIC rnoh_geolocation)

5. 修改 PackageProvider.cpp

打开 harmony/entry/src/main/cpp/PackageProvider.cpp,添加:

cpp 复制代码
#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
+ #include "GeoLocationPackage.h"

using namespace rnoh;

std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
    return {
        std::make_shared<RNOHGeneratedPackage>(ctx),
        + std::make_shared<GeoLocationPackage>(ctx),
    };
}

6. 在 ArkTs 侧引入 GeolocationPackage

打开 harmony/entry/src/main/ets/RNPackagesFactory.ts,添加:

typescript 复制代码
import type { RNPackageContext, RNPackage } from 'rnoh/ts';
+ import { GeoLocationPackage } from '@react-native-ohos/geolocation/ts';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    // ... 其他包
    + new GeoLocationPackage(ctx),
  ];
}

7. 同步并运行

点击 DevEco Studio 右上角的 sync 按钮,然后编译运行即可。

🔐 权限配置 ⭐

定位功能需要在 HarmonyOS 端配置相应的权限。

1. 配置 module.json5

打开 harmony/entry/src/main/module.json5,在 module 节点下添加权限配置:

json5 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:Access_location",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:Access_location",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      }
    ]
  }
}

2. 添加权限描述字符串

打开 harmony/entry/src/main/resources/base/element/string.json,添加:

json 复制代码
{
  "string": [
    {
      "name": "Access_location",
      "value": "用于获取您的地理位置信息,以提供更好的服务"
    }
  ]
}

权限说明

权限名称 说明 级别
ohos.permission.APPROXIMATELY_LOCATION 大致位置权限 normal
ohos.permission.LOCATION 精确位置权限 normal
ohos.permission.INTERNET 网络访问权限 normal

⚠️ 注意 : 如果需要后台定位功能,还需要添加 ohos.permission.LOCATION_IN_BACKGROUND 权限,该权限为 system_core 级别,需要系统签名。

📖 API 详解

🔷 setRNConfiguration - 设置全局配置 ⚙️

设置将在所有位置请求中使用的配置选项。

typescript 复制代码
Geolocation.setRNConfiguration(config: GeolocationConfiguration): void;

参数说明

参数 类型 必填 说明 HarmonyOS 支持
skipPermissionRequests boolean 是否跳过权限请求
authorizationLevel string 授权级别:autoalwayswhenInUse
enableBackgroundLocationUpdates boolean 是否启用后台位置更新
locationProvider string 位置提供者:autoandroidplayServices

⚠️ HarmonyOS 限制 : 仅支持 skipPermissionRequests 参数。

应用场景

typescript 复制代码
import Geolocation from '@react-native-community/geolocation';

// 配置跳过权限请求(适用于已自行处理权限的场景)
Geolocation.setRNConfiguration({
  skipPermissionRequests: true,
});

🔷 requestAuthorization - 请求权限 🔐

请求合适的位置权限。

typescript 复制代码
Geolocation.requestAuthorization(
  success?: () => void,
  error?: (error: GeolocationError) => void
): void;

参数说明

参数 类型 必填 说明
success function 权限授予回调
error function 权限拒绝回调

错误对象说明

属性 类型 说明 HarmonyOS 支持
code number 错误代码
message string 错误信息

应用场景

typescript 复制代码
import Geolocation from '@react-native-community/geolocation';
import { Alert } from 'react-native';

// 请求位置权限
const requestLocationPermission = () => {
  Geolocation.requestAuthorization(
    () => {
      console.log('位置权限已授予');
      // 权限授予后获取位置
      getCurrentLocation();
    },
    (error) => {
      console.log('位置权限被拒绝:', error.message);
      Alert.alert('提示', '需要位置权限才能使用定位功能');
    }
  );
};

🔷 getCurrentPosition - 获取当前位置 ⭐

使用最新位置信息调用成功回调一次。

typescript 复制代码
Geolocation.getCurrentPosition(
  success: (position: GeolocationPosition) => void,
  error?: (error: GeolocationError) => void,
  options?: GeolocationOptions
): void;

成功回调参数(position)

属性 类型 说明 HarmonyOS 支持
coords object 坐标信息
coords.latitude number 纬度
coords.longitude number 经度
coords.altitude number 海拔高度
coords.accuracy number 精度(米)
coords.altitudeAccuracy number 海拔精度
coords.heading number 方向(度)
coords.speed number 速度(米/秒)
timestamp number 时间戳

配置选项(options)

属性 类型 默认值 说明 HarmonyOS 支持
timeout number - 超时时间(毫秒)
maximumAge number - 可接受的缓存位置最大年龄(毫秒) ⚠️ 有延时问题
enableHighAccuracy boolean false 是否使用高精度定位

⚠️ HarmonyOS 限制 : altitudeAccuracy 不支持,maximumAge 存在延时问题。

应用场景

typescript 复制代码
import Geolocation from '@react-native-community/geolocation';
import { Alert } from 'react-native';

// 场景1:基础获取位置
const getCurrentLocation = () => {
  Geolocation.getCurrentPosition(
    (position) => {
      console.log('当前位置:', position);
      const { latitude, longitude } = position.coords;
      console.log(`纬度: ${latitude}, 经度: ${longitude}`);
    },
    (error) => {
      console.error('获取位置失败:', error.message);
      Alert.alert('错误', `获取位置失败: ${error.message}`);
    },
    {
      timeout: 15000,  // 15秒超时
      maximumAge: 10000,  // 接受10秒内的缓存位置
    }
  );
};

// 格式化时间戳的辅助函数
const formatTimestamp = (timestamp: number): string => {
  try {
    const ts = timestamp > 9999999999 ? timestamp : timestamp * 1000;
    const date = new Date(ts);
    if (isNaN(date.getTime())) {
      return '时间未知';
    }
    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
  } catch (e) {
    return '时间未知';
  }
};

// 场景2:获取位置并显示详细信息
const getLocationDetails = () => {
  Geolocation.getCurrentPosition(
    (position) => {
      const { coords, timestamp } = position;
      const locationInfo = {
        latitude: coords.latitude,
        longitude: coords.longitude,
        altitude: coords.altitude,
        accuracy: coords.accuracy,
        heading: coords.heading,
        speed: coords.speed,
        time: formatTimestamp(timestamp),
      };
      console.log('详细位置信息:', locationInfo);
    },
    (error) => {
      console.error('错误代码:', error.code);
      console.error('错误信息:', error.message);
    },
    {
      timeout: 20000,
    }
  );
};

// 场景3:检查位置服务是否可用
const checkAndGetLocation = async () => {
  try {
    const position = await new Promise((resolve, reject) => {
      Geolocation.getCurrentPosition(resolve, reject, {
        timeout: 10000,
      });
    });
    return position;
  } catch (error) {
    if (error.code === 1) {
      console.log('位置权限被拒绝');
    } else if (error.code === 2) {
      console.log('位置服务不可用');
    } else if (error.code === 3) {
      console.log('获取位置超时');
    }
    throw error;
  }
};

错误代码说明

错误代码 说明
1 权限被拒绝(PERMISSION_DENIED)
2 位置服务不可用(POSITION_UNAVAILABLE)
3 获取位置超时(TIMEOUT)

🔷 watchPosition - 持续监听位置 🔄

每当位置发生变化时调用成功回调,返回一个 watchId。

typescript 复制代码
Geolocation.watchPosition(
  success: (position: GeolocationPosition) => void,
  error?: (error: GeolocationError) => void,
  options?: GeolocationOptions
): number;

返回值number - watchId,用于后续清除监听

配置选项(options)

属性 类型 默认值 说明 HarmonyOS 支持
interval number - 位置更新的时间间隔(毫秒)
distanceFilter number 0 最小距离变化触发更新(米)
timeout number - 超时时间(毫秒)
enableHighAccuracy boolean false 是否使用高精度定位

⚠️ HarmonyOS 限制 : altitudeAccuracy 不支持。

应用场景

typescript 复制代码
import Geolocation from '@react-native-community/geolocation';
import { useState, useEffect, useCallback } from 'react';

// 定义位置信息类型(如果使用 TypeScript)
interface Position {
  coords: {
    latitude: number;
    longitude: number;
    altitude: number | null;
    accuracy: number;
    heading: number | null;
    speed: number | null;
  };
  timestamp: number;
}

// 场景1:基础位置监听
let watchId: number | null = null;

const startWatchingPosition = () => {
  watchId = Geolocation.watchPosition(
    (position: Position) => {
      console.log('位置更新:', position.coords);
    },
    (error: { code: number; message: string }) => {
      console.error('监听位置失败:', error.message);
    },
    {
      interval: 5000,  // 每5秒更新一次
      distanceFilter: 10,  // 移动超过10米才更新
    }
  );
  console.log('监听已启动,watchId:', watchId);
};

const stopWatchingPosition = () => {
  if (watchId !== null) {
    Geolocation.clearWatch(watchId);
    watchId = null;
    console.log('监听已停止');
  }
};

// 场景2:React Hook 封装
interface Position {
  coords: {
    latitude: number;
    longitude: number;
    altitude: number | null;
    accuracy: number;
    heading: number | null;
    speed: number | null;
  };
  timestamp: number;
}

interface PositionError {
  code: number;
  message: string;
}

interface WatchOptions {
  interval?: number;
  distanceFilter?: number;
  timeout?: number;
}

function useGeolocation(options?: WatchOptions) {
  const [position, setPosition] = useState<Position | null>(null);
  const [error, setError] = useState<PositionError | null>(null);
  const [loading, setLoading] = useState(false);

  const startWatch = useCallback(() => {
    setLoading(true);
    const id = Geolocation.watchPosition(
      (pos) => {
        setPosition(pos);
        setLoading(false);
      },
      (err) => {
        setError(err);
        setLoading(false);
      },
      options
    );
    return id;
  }, [options]);

  const stopWatch = useCallback((id: number) => {
    Geolocation.clearWatch(id);
  }, []);

  return { position, error, loading, startWatch, stopWatch };
}

// 使用示例
function LocationTracker() {
  const { position, error, loading, startWatch, stopWatch } = useGeolocation({
    interval: 3000,
    distanceFilter: 5,
  });
  const [watchId, setWatchId] = useState<number | null>(null);

  useEffect(() => {
    const id = startWatch();
    setWatchId(id);
    return () => {
      if (id !== null) {
        stopWatch(id);
      }
    };
  }, []);

  return (
    <View>
      {loading && <Text>获取位置中...</Text>}
      {error && <Text>错误: {error.message}</Text>}
      {position && (
        <Text>
          纬度: {position.coords.latitude.toFixed(6)}
          {'\n'}
          经度: {position.coords.longitude.toFixed(6)}
        </Text>
      )}
    </View>
  );
}

// 场景3:实时位置追踪与轨迹记录
function LocationTracker() {
  const [positions, setPositions] = useState<Position[]>([]);
  const [watchId, setWatchId] = useState<number | null>(null);
  const [isTracking, setIsTracking] = useState(false);

  const startTracking = () => {
    const id = Geolocation.watchPosition(
      (position) => {
        setPositions((prev) => [...prev, position]);
      },
      (error) => {
        console.error('追踪错误:', error);
        setIsTracking(false);
      },
      {
        interval: 2000,
        distanceFilter: 5,
      }
    );
    setWatchId(id);
    setIsTracking(true);
  };

  const stopTracking = () => {
    if (watchId !== null) {
      Geolocation.clearWatch(watchId);
      setWatchId(null);
    }
    setIsTracking(false);
  };

  return (
    <View>
      <Button
        title={isTracking ? '停止追踪' : '开始追踪'}
        onPress={isTracking ? stopTracking : startTracking}
      />
      <Text>已记录 {positions.length} 个位置点</Text>
    </View>
  );
}

🔷 clearWatch - 清除位置监听 🛑

通过 watchPosition() 返回的 ID 清除观察者。

typescript 复制代码
Geolocation.clearWatch(watchId: number): void;

参数说明

参数 类型 必填 说明
watchId number watchPosition 返回的监听ID

⚠️ HarmonyOS 限制: watchId 仅支持默认值 0。

应用场景

typescript 复制代码
import Geolocation from '@react-native-community/geolocation';
import { useEffect, useRef } from 'react';

// 场景:组件卸载时自动清除监听
function useLocationWatcher() {
  const watchIdRef = useRef<number | null>(null);

  useEffect(() => {
    return () => {
      // 组件卸载时清除监听
      if (watchIdRef.current !== null) {
        Geolocation.clearWatch(watchIdRef.current);
      }
    };
  }, []);

  const startWatching = () => {
    watchIdRef.current = Geolocation.watchPosition(
      (position) => console.log(position),
      (error) => console.error(error)
    );
  };

  const stopWatching = () => {
    if (watchIdRef.current !== null) {
      Geolocation.clearWatch(watchIdRef.current);
      watchIdRef.current = null;
    }
  };

  return { startWatching, stopWatching };
}

🔷 stopObserving - 停止所有监听 ⚠️

停止所有位置监听观察者。

typescript 复制代码
Geolocation.stopObserving(): void;

⚠️ HarmonyOS 限制: 此方法不支持。

iOS/Android 使用示例

typescript 复制代码
// 停止所有监听(HarmonyOS 不支持)
Geolocation.stopObserving();

💻 完整代码示例

下面是一个完整的示例,展示了 Geolocation 的各种功能应用:

typescript 复制代码
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  SafeAreaView,
  TouchableOpacity,
  Alert,
  ActivityIndicator,
  Platform,
} from 'react-native';
import Geolocation from '@react-native-community/geolocation';

// 定义类型接口
interface Position {
  coords: {
    latitude: number;
    longitude: number;
    altitude: number | null;
    accuracy: number;
    heading: number | null;
    speed: number | null;
  };
  timestamp: number;
}

interface PositionError {
  code: number;
  message: string;
}

interface WatchOptions {
  timeout?: number;
  maximumAge?: number;
  interval?: number;
  distanceFilter?: number;
}

interface LocationInfo {
  latitude: number;
  longitude: number;
  altitude: number | null;
  accuracy: number;
  heading: number | null;
  speed: number | null;
  timestamp: string;
}

function GeolocationDemo() {
  const [currentPosition, setCurrentPosition] = useState<LocationInfo | null>(null);
  const [watchPositions, setWatchPositions] = useState<LocationInfo[]>([]);
  const [isWatching, setIsWatching] = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const watchIdRef = useRef<number | null>(null);

  // 格式化日期时间
  const formatDateTime = (timestamp: number): string => {
    try {
      // timestamp 可能是毫秒或秒,需要判断
      const ts = timestamp > 9999999999 ? timestamp : timestamp * 1000;
      const date = new Date(ts);
    
      // 检查日期是否有效
      if (isNaN(date.getTime())) {
        return '时间未知';
      }
    
      const year = date.getFullYear();
      const month = String(date.getMonth() + 1).padStart(2, '0');
      const day = String(date.getDate()).padStart(2, '0');
      const hours = String(date.getHours()).padStart(2, '0');
      const minutes = String(date.getMinutes()).padStart(2, '0');
      const seconds = String(date.getSeconds()).padStart(2, '0');
    
      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    } catch (e) {
      return '时间未知';
    }
  };

  // 格式化位置信息
  const formatPosition = (position: Position): LocationInfo => {
    return {
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      altitude: position.coords.altitude,
      accuracy: position.coords.accuracy,
      heading: position.coords.heading,
      speed: position.coords.speed,
      timestamp: formatDateTime(position.timestamp),
    };
  };

  // 设置配置
  useEffect(() => {
    Geolocation.setRNConfiguration({
      skipPermissionRequests: false,
    });
  }, []);

  // 请求权限
  const requestPermission = useCallback(() => {
    return new Promise<void>((resolve, reject) => {
      Geolocation.requestAuthorization(
        () => {
          console.log('权限已授予');
          resolve();
        },
        (err: PositionError) => {
          console.log('权限被拒绝:', err.message);
          reject(err);
        }
      );
    });
  }, []);

  // 获取当前位置
  const getCurrentPosition = useCallback(async () => {
    setLoading(true);
    setError(null);

    try {
      await requestPermission();

      Geolocation.getCurrentPosition(
        (position) => {
          setCurrentPosition(formatPosition(position));
          setLoading(false);
        },
        (err: PositionError) => {
          setError(`错误 ${err.code}: ${err.message}`);
          setLoading(false);
        },
        {
          timeout: 15000,
          maximumAge: 10000,
        } as WatchOptions
      );
    } catch (err) {
      setError('无法获取位置权限');
      setLoading(false);
    }
  }, [requestPermission]);

  // 开始持续定位
  const startWatching = useCallback(async () => {
    try {
      await requestPermission();

      watchIdRef.current = Geolocation.watchPosition(
        (position) => {
          const locationInfo = formatPosition(position);
          setWatchPositions((prev) => {
            // 保留最近 20 条记录
            const newPositions = [...prev, locationInfo];
            return newPositions.slice(-20);
          });
        },
        (err: PositionError) => {
          Alert.alert('定位错误', err.message);
          setIsWatching(false);
        },
        {
          interval: 3000,
          distanceFilter: 10,
        } as WatchOptions
      );

      setIsWatching(true);
    } catch (err) {
      Alert.alert('权限错误', '无法获取位置权限');
    }
  }, [requestPermission]);

  // 停止持续定位
  const stopWatching = useCallback(() => {
    if (watchIdRef.current !== null) {
      Geolocation.clearWatch(watchIdRef.current);
      watchIdRef.current = null;
    }
    setIsWatching(false);
  }, []);

  // 清除历史记录
  const clearHistory = useCallback(() => {
    setWatchPositions([]);
  }, []);

  // 组件卸载时清理
  useEffect(() => {
    return () => {
      if (watchIdRef.current !== null) {
        Geolocation.clearWatch(watchIdRef.current);
      }
    };
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
        <Text style={styles.title}>📍 地理位置定位演示</Text>

        {/* 控制面板 */}
        <View style={styles.controlPanel}>
          <Text style={styles.panelTitle}>⚙️ 控制面板</Text>

          <TouchableOpacity
            style={[styles.button, styles.primaryButton]}
            onPress={getCurrentPosition}
            disabled={loading}
          >
            {loading ? (
              <ActivityIndicator color="#fff" />
            ) : (
              <Text style={styles.buttonText}>📍 获取当前位置</Text>
            )}
          </TouchableOpacity>

          <TouchableOpacity
            style={[
              styles.button,
              isWatching ? styles.dangerButton : styles.successButton,
            ]}
            onPress={isWatching ? stopWatching : startWatching}
          >
            <Text style={styles.buttonText}>
              {isWatching ? '⏹️ 停止监听' : '▶️ 开始监听'}
            </Text>
          </TouchableOpacity>

          {watchPositions.length > 0 && (
            <TouchableOpacity
              style={[styles.button, styles.warningButton]}
              onPress={clearHistory}
            >
              <Text style={styles.buttonText}>🗑️ 清除历史</Text>
            </TouchableOpacity>
          )}
        </View>

        {/* 错误提示 */}
        {error && (
          <View style={styles.errorContainer}>
            <Text style={styles.errorText}>❌ {error}</Text>
          </View>
        )}

        {/* 当前位置 */}
        {currentPosition && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>📍 当前位置</Text>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>纬度</Text>
              <Text style={styles.infoValue}>
                {currentPosition.latitude.toFixed(6)}°
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>经度</Text>
              <Text style={styles.infoValue}>
                {currentPosition.longitude.toFixed(6)}°
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>海拔</Text>
              <Text style={styles.infoValue}>
                {currentPosition.altitude?.toFixed(2) || 'N/A'} m
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>精度</Text>
              <Text style={styles.infoValue}>
                {currentPosition.accuracy.toFixed(2)} m
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>方向</Text>
              <Text style={styles.infoValue}>
                {currentPosition.heading?.toFixed(2) || 'N/A'}°
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>速度</Text>
              <Text style={styles.infoValue}>
                {currentPosition.speed?.toFixed(2) || 'N/A'} m/s
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>时间</Text>
              <Text style={styles.infoValue}>{currentPosition.timestamp}</Text>
            </View>
          </View>
        )}

        {/* 位置历史 */}
        {watchPositions.length > 0 && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>
              📊 位置历史 ({watchPositions.length} 条)
            </Text>
            {watchPositions.map((pos, index) => (
              <View key={index} style={styles.historyItem}>
                <View style={styles.historyHeader}>
                  <Text style={styles.historyIndex}>#{index + 1}</Text>
                  <Text style={styles.historyTime}>{pos.timestamp}</Text>
                </View>
                <Text style={styles.historyCoords}>
                  📍 {pos.latitude.toFixed(6)}, {pos.longitude.toFixed(6)}
                </Text>
                <Text style={styles.historyDetails}>
                  精度: {pos.accuracy.toFixed(1)}m | 
                  速度: {pos.speed?.toFixed(1) || 'N/A'}m/s
                </Text>
              </View>
            ))}
          </View>
        )}

        {/* 使用提示 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>💡 使用提示</Text>
          <Text style={styles.tipText}>• 首次使用需要授予位置权限</Text>
          <Text style={styles.tipText}>• 室内定位精度可能较低</Text>
          <Text style={styles.tipText}>• 持续定位会增加电量消耗</Text>
          <Text style={styles.tipText}>• 部分功能在 HarmonyOS 上有限制</Text>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  scrollView: {
    flex: 1,
  },
  scrollContent: {
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
    textAlign: 'center',
    marginBottom: 20,
  },
  controlPanel: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  panelTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 12,
  },
  button: {
    paddingVertical: 14,
    paddingHorizontal: 20,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 10,
  },
  primaryButton: {
    backgroundColor: '#007AFF',
  },
  successButton: {
    backgroundColor: '#34C759',
  },
  dangerButton: {
    backgroundColor: '#FF3B30',
  },
  warningButton: {
    backgroundColor: '#FF9500',
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
  errorContainer: {
    backgroundColor: '#FFE5E5',
    borderRadius: 8,
    padding: 12,
    marginBottom: 16,
  },
  errorText: {
    color: '#FF3B30',
    fontSize: 14,
  },
  section: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 12,
  },
  infoRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#f0f0f0',
  },
  infoLabel: {
    fontSize: 14,
    color: '#666',
  },
  infoValue: {
    fontSize: 14,
    color: '#333',
    fontWeight: '500',
  },
  historyItem: {
    backgroundColor: '#f9f9f9',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
  },
  historyHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 4,
  },
  historyIndex: {
    fontSize: 12,
    fontWeight: 'bold',
    color: '#007AFF',
  },
  historyTime: {
    fontSize: 12,
    color: '#999',
  },
  historyCoords: {
    fontSize: 14,
    color: '#333',
    marginBottom: 2,
  },
  historyDetails: {
    fontSize: 12,
    color: '#666',
  },
  tipText: {
    fontSize: 14,
    color: '#666',
    lineHeight: 22,
  },
});

export default GeolocationDemo;

⚠️ 注意事项与最佳实践

1. 权限处理

typescript 复制代码
// ✅ 推荐:先检查权限再获取位置
const getLocationWithPermission = async () => {
  try {
    // 请求权限
    await new Promise((resolve, reject) => {
      Geolocation.requestAuthorization(resolve, reject);
    });
  
    // 获取位置
    const position = await new Promise((resolve, reject) => {
      Geolocation.getCurrentPosition(resolve, reject, { timeout: 10000 });
    });
  
    return position;
  } catch (error) {
    console.error('权限或定位失败:', error);
    throw error;
  }
};

2. 错误处理

typescript 复制代码
// ✅ 推荐:完善的错误处理
const handleLocationError = (error: GeolocationError) => {
  switch (error.code) {
    case 1: // PERMISSION_DENIED
      Alert.alert('权限被拒绝', '请在设置中开启位置权限');
      break;
    case 2: // POSITION_UNAVAILABLE
      Alert.alert('位置不可用', '请检查是否开启定位服务');
      break;
    case 3: // TIMEOUT
      Alert.alert('定位超时', '请检查网络连接后重试');
      break;
    default:
      Alert.alert('定位错误', error.message);
  }
};

3. 性能优化

typescript 复制代码
// ✅ 推荐:合理设置监听参数
Geolocation.watchPosition(
  successCallback,
  errorCallback,
  {
    interval: 5000,      // 5秒更新一次,避免频繁请求
    distanceFilter: 10,  // 移动超过10米才更新,减少无效更新
  }
);

// ✅ 推荐:及时清除监听
useEffect(() => {
  const watchId = Geolocation.watchPosition(...);
  return () => Geolocation.clearWatch(watchId);
}, []);

4. 常见问题排查

问题 1: 权限被拒绝

  • 检查 module.json5 是否正确配置了权限
  • 确认用户在系统设置中授予了位置权限
  • 检查权限描述字符串是否配置

问题 2: 获取位置超时

  • 检查设备是否开启了定位服务
  • 尝试在开阔地带测试(室内GPS信号弱)
  • 增加超时时间设置

问题 3: 位置精度差

  • 室内环境精度较低是正常现象
  • 可以尝试使用 enableHighAccuracy: true(HarmonyOS 不支持)
  • 检查设备的定位模式设置

问题 4: watchPosition 不更新

  • 检查 distanceFilter 设置是否过大
  • 确认设备位置确实发生了变化
  • 检查是否有其他应用占用了定位服务

⚠️ 已知问题

1. maximumAge 延时问题

maximumAge 属性在 HarmonyOS 上存在延时问题。

Issue : issue#6

替代方案:自行实现缓存逻辑

typescript 复制代码
let lastPosition: GeolocationPosition | null = null;
let lastPositionTime = 0;

const getCachedPosition = (maxAge: number) => {
  const now = Date.now();
  if (lastPosition && (now - lastPositionTime) < maxAge) {
    return lastPosition;
  }
  return null;
};

const getPositionWithCache = async (maxAge: number = 10000) => {
  const cached = getCachedPosition(maxAge);
  if (cached) {
    return cached;
  }
  
  return new Promise((resolve, reject) => {
    Geolocation.getCurrentPosition(
      (position) => {
        lastPosition = position;
        lastPositionTime = Date.now();
        resolve(position);
      },
      reject,
      { timeout: 15000 }
    );
  });
};

2. 部分属性不支持

属性 说明 状态
altitudeAccuracy 海拔精度 ❌ 不支持
enableHighAccuracy 高精度定位 ❌ 不支持
stopObserving 停止所有监听 ❌ 不支持
authorizationLevel 授权级别 ❌ 不支持
enableBackgroundLocationUpdates 后台定位 ❌ 不支持

🧪 测试验证

1. 测试要点

  • 权限请求: 确认首次使用时正确请求权限
  • 获取位置 : 验证 getCurrentPosition 返回正确的位置信息
  • 持续监听 : 测试 watchPosition 能正确接收位置更新
  • 清除监听 : 验证 clearWatch 能正确停止监听
  • 错误处理: 测试各种错误场景的处理

2. 常见问题排查

问题 1: 编译报错找不到模块

  • 检查 oh-package.json5 是否正确配置依赖
  • 执行 ohpm install 同步依赖
  • 检查 CMakeLists.txt 配置

问题 2: 运行时报错

  • 检查 RNPackagesFactory.ts 是否正确引入 Package
  • 确认 PackageProvider.cpp 是否正确引入头文件

问题 3: 定位不工作

  • 检查设备是否开启定位服务
  • 确认应用是否获得位置权限
  • 检查权限配置是否完整

📊 API 支持情况总览

方法支持

方法 说明 HarmonyOS 支持
setRNConfiguration 设置全局配置 ⚠️ 部分支持
requestAuthorization 请求位置权限 ⚠️ 部分支持
getCurrentPosition 获取当前位置 ⚠️ 部分支持
watchPosition 持续监听位置 ⚠️ 部分支持
clearWatch 清除位置监听
stopObserving 停止所有监听

配置选项支持

属性 方法 HarmonyOS 支持
skipPermissionRequests setRNConfiguration
authorizationLevel setRNConfiguration
enableBackgroundLocationUpdates setRNConfiguration
timeout getCurrentPosition
maximumAge getCurrentPosition ⚠️ 有延时问题
enableHighAccuracy getCurrentPosition
interval watchPosition
distanceFilter watchPosition

位置信息支持

属性 说明 HarmonyOS 支持
latitude 纬度
longitude 经度
altitude 海拔
accuracy 精度
altitudeAccuracy 海拔精度
heading 方向
speed 速度
timestamp 时间戳

📝 总结

通过集成 @react-native-community/geolocation,我们为项目添加了完整的地理位置定位能力。该库提供了获取当前位置和持续监听位置变化的 API,是开发地图导航、位置追踪、签到打卡等位置相关应用的基础组件。

相关推荐
数据潜水员2 小时前
解决el-carousel 前后图片切换闪烁问题
前端·javascript·vue.js
optimistic_chen2 小时前
【Vue入门】scoped与组件通信
linux·前端·javascript·vue.js·前端框架·组件通信
SuperEugene2 小时前
前端空值处理规范:Vue 实战避坑,可选链、?? 兜底写法|项目规范篇
前端·javascript·vue.js
前端百草阁2 小时前
Vue3 Diff 算法详解
前端·javascript·vue.js·算法·前端框架
im_AMBER2 小时前
前后端对接: ESM配置与React Router
前端·javascript·学习·react.js·性能优化·前端框架·ecmascript
学且思2 小时前
使用import.meta.url实现传递路径动态加载资源
前端·javascript·vue.js
problc2 小时前
OpenClaw 的前端用的React还是Vue?
前端·vue.js·react.js
冰暮流星2 小时前
javascript里面的return语句讲解
开发语言·前端·javascript
弓.长.2 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:react-native-image-gallery — 图片画廊组件
react native·react.js·harmonyos