ReactNative项目OpenHarmony三方库集成实战:react-native-amap3d(推荐使用react-native-maps)

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

📋 前言

地图功能是移动应用中最常见且重要的功能之一,无论是出行导航、位置分享、周边搜索还是物流配送,都离不开地图组件的支持。react-native-amap3d 是一个基于高德地图最新 3D SDK 的 React Native 组件库,提供了丰富的地图功能和覆盖物组件,支持地图显示、标记点、折线、多边形、圆形、热力图、点聚合等功能,是开发位置服务应用的利器。

但是我并不推荐这个库的使用,这个地图库遗留了很多问题没有处理,而且地图拖动较卡,我更推荐harmony自带的Map-Kit,可以看我的另一篇文章:https://blog.csdn.net/qq_61024956/article/details/159465668

🎯 库简介

基本信息

  • 库名称 : react-native-amap3d
  • 版本信息 :
    • 3.2.4 + @react-native-oh-tpl/react-native-amap3d: 支持 RN 0.72 版本
    • 3.2.5 + @react-native-ohos/react-native-amap3d: 支持 RN 0.77 版本
  • 官方仓库: https://github.com/qiuxiang/react-native-amap3d
  • 鸿蒙仓库: https://github.com/react-native-oh-library/react-native-amap3d
  • 主要功能 :
    • 🗺️ 地图模式切换(常规、卫星、导航、夜间)
    • 🏢 3D 建筑、路况、室内地图
    • 📍 地图标记(Marker)
    • 📏 折线绘制(Polyline)
    • 🔷 多边形绘制(Polygon)
    • ⭕ 圆形绘制(Circle)
    • 🔥 热力图(HeatMap)
    • 📌 点聚合(Cluster)

为什么需要高德地图组件?

特性 原生开发 react-native-amap3d
跨平台一致性 ⚠️ 需分别适配 ✅ 统一接口
3D地图渲染 ⚠️ 需原生SDK ✅ 内置3D支持
覆盖物组件 ❌ 需自行实现 ✅ 丰富的组件库
点聚合功能 ❌ 需额外开发 ✅ 内置Cluster组件
手势交互 ⚠️ 需自行处理 ✅ 自动支持
HarmonyOS 支持 ❌ 无 ✅ 完善适配

核心功能

功能 说明 HarmonyOS 支持
MapView 地图视图组件
Marker 地图标记点
Polyline 折线绘制
Polygon 多边形绘制
Circle 圆形覆盖物
Cluster 点聚合 ⚠️ 类型问题
HeatMap 热力图 ⚠️ 部分支持
MultiPoint 海量点 ⚠️ 部分支持

兼容性验证

在以下环境验证通过:

  • RNOH : 0.72.90; SDK : HarmonyOS6.0.0 (API Version 20 Release); IDE : DevEco Studio 6.0.2; ROM: HarmonyOS 6.0.0

📦 安装步骤

1. 安装依赖

请到三方库的 Releases 发布地址查看配套的版本信息:

三方库版本 发布信息 支持 RN 版本
3.2.4 @react-native-oh-tpl/react-native-amap3d 0.72
3.2.5 @react-native-ohos/react-native-amap3d 0.77
bash 复制代码
# RN 0.72 版本
npm install @react-native-oh-tpl/react-native-amap3d@3.2.4-0.1.1-rc.1

# 或者使用 yarn
yarn add @react-native-oh-tpl/react-native-amap3d

2. 验证安装

安装完成后,检查 package.json 文件:

json 复制代码
{
  "dependencies": {
    "@react-native-oh-tpl/react-native-amap3d": "^3.2.4-0.1.1-rc.1"
  }
}

🔧 HarmonyOS 平台配置 ⭐

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

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

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

2. 引入原生端代码

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

json5 复制代码
"dependencies": {
  "@react-native-oh-tpl/react-native-amap3d": "file:../../node_modules/@react-native-oh-tpl/react-native-amap3d/harmony/rn_amap3d.har"
}

3. 同步依赖

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

bash 复制代码
cd entry
ohpm install

4. 配置 CMakeLists

打开 entry/src/main/cpp/CMakeLists.txt,添加:

c 复制代码
project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
+ set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../react-native-harmony/harmony/cpp")

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

# RNOH_BEGIN: manual_package_linking_1
add_subdirectory("../../../../sample_package/src/main/cpp" ./sample-package)
+ add_subdirectory("${OH_MODULES}/@react-native-oh-tpl/react-native-amap3d/src/main/cpp" ./amap3d)
# RNOH_END: manual_package_linking_1

add_library(rnoh_app SHARED
    "./PackageProvider.cpp"
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
)

target_link_libraries(rnoh_app PUBLIC rnoh)

# RNOH_BEGIN: manual_package_linking_2
target_link_libraries(rnoh_app PUBLIC rnoh_sample_package)
+ target_link_libraries(rnoh_app PUBLIC rnoh_amap3d)
# RNOH_END: manual_package_linking_2

5. 引入 MapViewPackage

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

cpp 复制代码
#include "RNOH/PackageProvider.h"
#include "SamplePackage.h"
+ #include "MapViewPackage.h"

using namespace rnoh;

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

6. 在 ArkTS 侧引入 MapViewPackage

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

typescript 复制代码
import { AMap3DPackage } from '@react-native-ohos/react-native-amap3d/ts';

export function createRNPackages(ctx: RNPackageContext) {
  return [
    new AMap3DPackage(ctx),
  ];
}

7. 在 index.ets 中配置组件

找到 function buildCustomRNComponent(),一般位于 entry/src/main/ets/pages/index.etsentry/src/main/ets/rn/LoadBundle.ets,添加:

typescript 复制代码
import {
  A_MAP_CIRCLE_VIEW_TYPE,
  A_MAP_MARKER_TYPE,
  A_MAP_POLYGON_TYPE,
  A_MAP_POLYLINE_TYPE,
  AMapCircle,
  AMapMarker,
  AMapPolygon,
  AMapPolyline,
  AMapView,
  GOADE_MAP_VIEW_TYPE
} from '@react-native-ohos/react-native-amap3d';

@Builder
export function buildCustomRNComponent(ctx: ComponentBuilderContext) {
  // ... 其他组件
  if (ctx.componentName === GOADE_MAP_VIEW_TYPE) {
    AMapView({
      ctx: ctx.rnComponentContext,
      tag: ctx.tag,
    })
  }
  if (ctx.componentName === A_MAP_CIRCLE_VIEW_TYPE) {
    AMapCircle({
      ctx: ctx.rnComponentContext,
      tag: ctx.tag,
    })
  }
  if (ctx.componentName === A_MAP_MARKER_TYPE) {
    AMapMarker({
      ctx: ctx.rnComponentContext,
      tag: ctx.tag,
    })
  }
  if (ctx.componentName === A_MAP_POLYGON_TYPE) {
    AMapPolygon({
      ctx: ctx.rnComponentContext,
      tag: ctx.tag,
    })
  }
  if (ctx.componentName === A_MAP_POLYLINE_TYPE) {
    AMapPolyline({
      ctx: ctx.rnComponentContext,
      tag: ctx.tag,
    })
  }
}

💡 提示:本库使用了混合方案,需要添加组件名。

entry/src/main/ets/pages/index.etsentry/src/main/ets/rn/LoadBundle.ets 找到常量 arkTsComponentNames,在其数组里添加组件名:

typescript 复制代码
const arkTsComponentNames: Array<string> = [
  // ... 其他组件名
  A_MAP_CIRCLE_VIEW_TYPE,
  A_MAP_MARKER_TYPE,
  A_MAP_POLYGON_TYPE,
  A_MAP_POLYLINE_TYPE,
  GOADE_MAP_VIEW_TYPE,
];

然后编译、运行即可。

📖 API 详解

MapView - 地图视图组件

地图视图是所有地图功能的基础容器,用于显示地图和处理用户交互。

mapType - 地图类型

设置地图的显示类型,支持多种地图模式。

类型number

可选值

说明
1 标准地图
2 卫星地图
3 夜间地图
4 导航地图

使用场景

  • 标准地图:日常浏览、位置展示
  • 卫星地图:地理分析、户外导航
  • 夜间地图:夜间模式、降低亮度
  • 导航地图:路线导航、实时路况
tsx 复制代码
import { MapView } from 'react-native-amap3d';

// 标准地图
<MapView mapType={1} style={{ flex: 1 }} />

// 卫星地图
<MapView mapType={2} style={{ flex: 1 }} />

// 夜间地图
<MapView mapType={3} style={{ flex: 1 }} />

initialCameraPosition - 初始相机位置

设置地图初始化时的视角位置,包括中心点和缩放级别。

类型CameraPosition

CameraPosition 结构

属性 类型 说明
target LatLng 地图中心点坐标
zoom number 缩放级别 (3-20)
tilt number 倾斜角度 (0-60)
bearing number 旋转角度 (0-360)

使用场景

  • 定位到特定城市
  • 显示用户当前位置
  • 展示特定区域地图
tsx 复制代码
<MapView
  style={{ flex: 1 }}
  initialCameraPosition={{
    target: {
      latitude: 39.91095,
      longitude: 116.37296,
    },
    zoom: 12,
    tilt: 30,
    bearing: 0,
  }}
/>

// 定位到上海
<MapView
  style={{ flex: 1 }}
  initialCameraPosition={{
    target: {
      latitude: 31.2304,
      longitude: 121.4737,
    },
    zoom: 10,
  }}
/>

myLocationEnabled - 显示定位点

是否显示用户当前位置的蓝色定位点。

类型boolean

默认值false

使用场景

  • 显示用户当前位置
  • 导航应用
  • 周边搜索
tsx 复制代码
// 启用定位
<MapView
  myLocationEnabled={true}
  style={{ flex: 1 }}
/>

// 结合定位按钮
<MapView
  myLocationEnabled={true}
  myLocationButtonEnabled={true}
  style={{ flex: 1 }}
/>

⚠️ 注意:使用定位功能需要申请定位权限。


onPress - 点击事件

用户点击地图时触发的回调函数。

类型(event: NativeSyntheticEvent<LatLng>) => void

参数

  • event.nativeEvent.latitude: 点击位置的纬度
  • event.nativeEvent.longitude: 点击位置的经度

使用场景

  • 选择位置
  • 添加标记点
  • 获取点击位置信息
tsx 复制代码
const [markerPosition, setMarkerPosition] = useState<LatLng | null>(null);

<MapView
  style={{ flex: 1 }}
  onPress={(event) => {
    const { latitude, longitude } = event.nativeEvent;
    console.log('点击位置:', latitude, longitude);
    setMarkerPosition({ latitude, longitude });
  }}
>
  {markerPosition && (
    <Marker position={markerPosition} />
  )}
</MapView>

onLongPress - 长按事件

用户长按地图时触发的回调函数。

类型(event: NativeSyntheticEvent<LatLng>) => void

使用场景

  • 长按选择位置
  • 显示位置详情
  • 添加收藏点
tsx 复制代码
<MapView
  style={{ flex: 1 }}
  onLongPress={(event) => {
    const { latitude, longitude } = event.nativeEvent;
    Alert.alert('长按位置', `${latitude}, ${longitude}`);
  }}
/>

onLoad - 地图加载完成

地图加载完成后触发的回调函数。

类型(event: NativeSyntheticEvent<voidEvent>) => void

使用场景

  • 地图加载完成后执行操作
  • 初始化地图状态
  • 请求数据
tsx 复制代码
<MapView
  style={{ flex: 1 }}
  onLoad={() => {
    console.log('地图加载完成');
    // 可以在这里执行初始化操作
  }}
/>

onCameraMove / onCameraIdle - 相机移动事件

监听地图视角的变化。

类型(event: NativeSyntheticEvent<CameraPosition>) => void

使用场景

  • 实时获取地图中心点
  • 动态加载数据
  • 点聚合更新
tsx 复制代码
<MapView
  style={{ flex: 1 }}
  onCameraMove={(event) => {
    console.log('相机移动中:', event.nativeEvent);
  }}
  onCameraIdle={(event) => {
    console.log('相机停止:', event.nativeEvent);
    // 可以在这里更新点聚合
  }}
/>

Marker - 地图标记

在地图上显示位置标记,支持自定义图标和拖拽功能。

position - 标记位置

设置标记点的经纬度坐标。

类型LatLng

使用场景

  • 显示地点位置
  • 标记兴趣点
  • 显示搜索结果
tsx 复制代码
import { MapView, Marker } from 'react-native-amap3d';

<MapView style={{ flex: 1 }}>
  <Marker
    position={{
      latitude: 39.906901,
      longitude: 116.397972,
    }}
  />
</MapView>

// 多个标记点
<MapView style={{ flex: 1 }}>
  <Marker position={{ latitude: 39.906901, longitude: 116.397972 }} />
  <Marker position={{ latitude: 39.916901, longitude: 116.407972 }} />
  <Marker position={{ latitude: 39.926901, longitude: 116.417972 }} />
</MapView>

icon - 自定义图标

设置标记点的自定义图标,支持本地图片和网络图片。

类型ImageSource

使用场景

  • 不同类型的标记使用不同图标
  • 品牌定制图标
  • 状态区分图标
tsx 复制代码
// 本地图片
<Marker
  position={{ latitude: 39.906901, longitude: 116.397972 }}
  icon={require('./images/marker.png')}
/>

// 网络图片
<Marker
  position={{ latitude: 39.906901, longitude: 116.397972 }}
  icon={{
    uri: 'https://example.com/marker.png',
    width: 40,
    height: 40,
  }}
/>

// 不同状态图标
const getMarkerIcon = (status: string) => {
  switch (status) {
    case 'active': return require('./images/active.png');
    case 'inactive': return require('./images/inactive.png');
    default: return require('./images/default.png');
  }
};

<Marker
  position={position}
  icon={getMarkerIcon(status)}
/>

draggable - 可拖拽

设置标记点是否可以拖拽移动。

类型boolean

默认值false

使用场景

  • 位置选择器
  • 编辑位置
  • 路线规划
tsx 复制代码
const [position, setPosition] = useState({
  latitude: 39.906901,
  longitude: 116.397972,
});

<MapView style={{ flex: 1 }}>
  <Marker
    position={position}
    draggable={true}
    onDragEnd={({ nativeEvent }) => {
      setPosition({
        latitude: nativeEvent.latitude,
        longitude: nativeEvent.longitude,
      });
      console.log('新位置:', nativeEvent);
    }}
  />
</MapView>

onPress - 点击标记

点击标记点时触发的回调函数。

类型() => void

使用场景

  • 显示地点详情
  • 导航到该位置
  • 编辑标记信息
tsx 复制代码
<Marker
  position={{ latitude: 39.906901, longitude: 116.397972 }}
  onPress={() => {
    Alert.alert('标记被点击', '显示详情弹窗');
  }}
/>

Polyline - 折线绘制

在地图上绘制折线,常用于路线展示。

points - 折线点集合

设置折线的坐标点数组。

类型LatLng[]

使用场景

  • 路线展示
  • 轨迹回放
  • 边界绘制
tsx 复制代码
import { MapView, Polyline } from 'react-native-amap3d';

const routePoints = [
  { latitude: 39.906901, longitude: 116.397972 },
  { latitude: 39.916901, longitude: 116.407972 },
  { latitude: 39.926901, longitude: 116.417972 },
];

<MapView style={{ flex: 1 }}>
  <Polyline
    points={routePoints}
    width={5}
    color="rgba(0, 122, 255, 0.8)"
  />
</MapView>

width / color - 线条样式

设置折线的宽度和颜色。

类型width: number, color: string

使用场景

  • 不同路线类型区分
  • 高亮显示特定路线
  • 美化地图展示
tsx 复制代码
// 粗线蓝色
<Polyline
  points={points}
  width={10}
  color="#007AFF"
/>

// 细线红色虚线效果
<Polyline
  points={points}
  width={3}
  color="rgba(255, 0, 0, 0.6)"
/>

Polygon - 多边形绘制

在地图上绘制多边形区域。

points - 多边形顶点

设置多边形的顶点坐标数组。

类型LatLng[]

使用场景

  • 区域标注
  • 电子围栏
  • 行政区划展示
tsx 复制代码
import { MapView, Polygon } from 'react-native-amap3d';

const areaPoints = [
  { latitude: 39.906901, longitude: 116.397972 },
  { latitude: 39.916901, longitude: 116.397972 },
  { latitude: 39.916901, longitude: 116.407972 },
  { latitude: 39.906901, longitude: 116.407972 },
];

<MapView style={{ flex: 1 }}>
  <Polygon
    points={areaPoints}
    strokeWidth={2}
    strokeColor="rgba(0, 122, 255, 0.8)"
    fillColor="rgba(0, 122, 255, 0.3)"
  />
</MapView>

fillColor / strokeColor - 填充和边框颜色

设置多边形的填充颜色和边框颜色。

类型string

tsx 复制代码
<Polygon
  points={points}
  strokeWidth={3}
  strokeColor="#FF0000"
  fillColor="rgba(255, 0, 0, 0.2)"
/>

Circle - 圆形覆盖物

在地图上绘制圆形区域。

center / radius - 圆心和半径

设置圆形的中心点和半径(单位:米)。

类型center: LatLng, radius: number

使用场景

  • 周边搜索范围
  • 定位精度展示
  • 覆盖范围标注
tsx 复制代码
import { MapView, Circle } from 'react-native-amap3d';

<MapView style={{ flex: 1 }}>
  <Circle
    center={{ latitude: 39.906901, longitude: 116.397972 }}
    radius={500}
    strokeWidth={2}
    strokeColor="rgba(0, 122, 255, 0.8)"
    fillColor="rgba(0, 122, 255, 0.3)"
  />
</MapView>

// 显示定位精度
<Circle
  center={userLocation}
  radius={accuracy}
  fillColor="rgba(0, 122, 255, 0.2)"
  strokeColor="rgba(0, 122, 255, 0.5)"
  strokeWidth={1}
/>

📋 完整示例

ts 复制代码
import React, { useRef, useState, useCallback } from "react";
import {
  SafeAreaView,
  StyleSheet,
  Text,
  View,
  Alert,
  TouchableOpacity,
  TextInput,
  FlatList,
} from "react-native";
import {
  MapView,
  Marker,
  Polyline,
  Circle,
} from "react-native-amap3d";

const mockPOIs = [
  { id: "1", name: "天安门广场", latitude: 39.9087, longitude: 116.3975, category: "景点" },
  { id: "2", name: "故宫博物院", latitude: 39.9163, longitude: 116.3972, category: "景点" },
  { id: "3", name: "王府井步行街", latitude: 39.9139, longitude: 116.4103, category: "购物" },
  { id: "4", name: "北京站", latitude: 39.9029, longitude: 116.4271, category: "交通" },
  { id: "5", name: "国贸商城", latitude: 39.9086, longitude: 116.4595, category: "购物" },
  { id: "6", name: "三里屯", latitude: 39.9324, longitude: 116.4535, category: "娱乐" },
  { id: "7", name: "鸟巢体育场", latitude: 39.9928, longitude: 116.3967, category: "景点" },
  { id: "8", name: "水立方", latitude: 39.9931, longitude: 116.3872, category: "景点" },
];

const mapTypes = [
  { label: "标准", value: 1 },
  { label: "卫星", value: 2 },
  { label: "夜间", value: 3 },
];

const App: React.FC = () => {
  const mapRef = useRef<any>(null);

  const [currentMapType, setCurrentMapType] = useState(1);
  const [zoomLevel, setZoomLevel] = useState(12);
  const [markers, setMarkers] = useState<Array<{ latitude: number; longitude: number; title?: string }>>([]);
  const [selectedPOI, setSelectedPOI] = useState<any>(null);
  const [showSearchPanel, setShowSearchPanel] = useState(false);
  const [searchText, setSearchText] = useState("");
  const [showMapTypeSelector, setShowMapTypeSelector] = useState(false);
  const [userLocation, setUserLocation] = useState<{ latitude: number; longitude: number } | null>(null);
  const [routePoints, setRoutePoints] = useState<Array<{ latitude: number; longitude: number }>>([]);
  const [isRouteMode, setIsRouteMode] = useState(false);

  const handleMapPress = useCallback((event: any) => {
    const { latitude, longitude } = event.nativeEvent;
    if (isRouteMode) {
      setRoutePoints((prev) => [...prev, { latitude, longitude }]);
    } else {
      setMarkers((prev) => [
        ...prev,
        { latitude, longitude, title: `标记 ${prev.length + 1}` },
      ]);
    }
  }, [isRouteMode]);

  const handleMarkerPress = useCallback((index: number) => {
    const marker = markers[index];
    Alert.alert("标记信息", `${marker.title || "未命名"}\n纬度: ${marker.latitude.toFixed(6)}\n经度: ${marker.longitude.toFixed(6)}`, [
      { text: "删除", onPress: () => removeMarker(index), style: "destructive" },
      { text: "导航到这里", onPress: () => startNavigation(marker) },
      { text: "取消", style: "cancel" },
    ]);
  }, [markers]);

  const removeMarker = useCallback((index: number) => {
    setMarkers((prev) => prev.filter((_, i) => i !== index));
  }, []);

  const startNavigation = useCallback((destination: any) => {
    const startLocation = userLocation || { latitude: 39.906901, longitude: 116.397972 };
    setRoutePoints([
      startLocation,
      { latitude: destination.latitude, longitude: destination.longitude },
    ]);
    mapRef.current?.moveCamera(
      {
        target: { latitude: destination.latitude, longitude: destination.longitude },
        zoom: 14,
      },
      500
    );
    Alert.alert("导航", `已规划从当前位置到 ${destination.title || "目的地"} 的路线`);
  }, [userLocation]);

  const clearAllMarkers = useCallback(() => {
    setMarkers([]);
    setRoutePoints([]);
    setSelectedPOI(null);
  }, []);

  const toggleMapType = useCallback((type: number) => {
    setCurrentMapType(type);
    setShowMapTypeSelector(false);
  }, []);

  const handleZoom = useCallback((delta: number) => {
    const newZoom = Math.max(3, Math.min(20, zoomLevel + delta));
    setZoomLevel(newZoom);
    mapRef.current?.moveCamera({ zoom: newZoom }, 300);
  }, [zoomLevel]);

  const locateUser = useCallback(() => {
    const location = { latitude: 39.906901, longitude: 116.397972 };
    setUserLocation(location);
    mapRef.current?.moveCamera(
      {
        target: location,
        zoom: 15,
      },
      500
    );
  }, []);

  const searchPOI = useCallback((text: string) => {
    setSearchText(text);
  }, []);

  const selectSearchResult = useCallback((poi: any) => {
    setSelectedPOI(poi);
    setShowSearchPanel(false);
    setSearchText("");
    mapRef.current?.moveCamera(
      {
        target: { latitude: poi.latitude, longitude: poi.longitude },
        zoom: 16,
      },
      500
    );
  }, []);

  const filteredPOIs = mockPOIs.filter((poi) =>
    poi.name.toLowerCase().includes(searchText.toLowerCase())
  );

  const toggleRouteMode = useCallback(() => {
    setIsRouteMode((prev) => !prev);
    setRoutePoints([]);
  }, []);

  const clearRoute = useCallback(() => {
    setRoutePoints([]);
  }, []);

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>高德地图示例</Text>
        <View style={styles.headerButtons}>
          <TouchableOpacity
            style={[styles.headerButton, isRouteMode && styles.headerButtonActive]}
            onPress={toggleRouteMode}
          >
            <Text style={styles.headerButtonText}>{isRouteMode ? "退出路线" : "规划路线"}</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.headerButton} onPress={() => setShowSearchPanel(true)}>
            <Text style={styles.headerButtonText}>搜索</Text>
          </TouchableOpacity>
        </View>
      </View>

      <MapView
        ref={mapRef}
        style={styles.map}
        mapType={currentMapType}
        initialCameraPosition={{
          target: { latitude: 39.906901, longitude: 116.397972 },
          zoom: zoomLevel,
        }}
        myLocationEnabled={true}
        labelsEnabled={true}
        compassEnabled={true}
        scaleControlsEnabled={true}
        trafficEnabled={false}
        onPress={handleMapPress}
        onLongPress={(event: any) => {
          const { latitude, longitude } = event.nativeEvent;
          Alert.alert("位置信息", `纬度: ${latitude.toFixed(6)}\n经度: ${longitude.toFixed(6)}`);
        }}
        onLoad={() => console.log("地图加载完成")}
        onCameraMove={(event: any) => {
          setZoomLevel(event.nativeEvent.zoom);
        }}
      >
        {userLocation && (
          <Circle
            center={userLocation}
            radius={100}
            strokeWidth={2}
            strokeColor="rgba(0, 122, 255, 0.8)"
            fillColor="rgba(0, 122, 255, 0.2)"
          />
        )}

        {markers.map((marker, index) => (
          <Marker
            key={`marker-${index}`}
            position={{ latitude: marker.latitude, longitude: marker.longitude }}
            onPress={() => handleMarkerPress(index)}
          />
        ))}

        {selectedPOI && (
          <Marker
            position={{ latitude: selectedPOI.latitude, longitude: selectedPOI.longitude }}
            onPress={() => {
              Alert.alert(selectedPOI.name, `分类: ${selectedPOI.category}`);
            }}
          />
        )}

        {routePoints.length > 1 && (
          <Polyline
            points={routePoints}
            width={6}
            color="rgba(0, 122, 255, 0.9)"
          />
        )}

        {routePoints.length > 0 && (
          <Marker position={routePoints[0]} />
        )}
        {routePoints.length > 1 && (
          <Marker position={routePoints[routePoints.length - 1]} />
        )}
      </MapView>

      <View style={styles.mapControls}>
        <View style={styles.zoomControls}>
          <TouchableOpacity
            style={styles.zoomButton}
            onPress={() => handleZoom(1)}
          >
            <Text style={styles.zoomButtonText}>+</Text>
          </TouchableOpacity>
          <View style={styles.zoomDivider} />
          <TouchableOpacity
            style={styles.zoomButton}
            onPress={() => handleZoom(-1)}
          >
            <Text style={styles.zoomButtonText}>-</Text>
          </TouchableOpacity>
        </View>

        <TouchableOpacity
          style={styles.controlButton}
          onPress={() => setShowMapTypeSelector(true)}
        >
          <Text style={styles.controlButtonText}>图层</Text>
        </TouchableOpacity>

        <TouchableOpacity style={styles.controlButton} onPress={locateUser}>
          <Text style={styles.controlButtonText}>定位</Text>
        </TouchableOpacity>

        <TouchableOpacity style={styles.controlButton} onPress={clearAllMarkers}>
          <Text style={styles.controlButtonText}>清除</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.infoPanel}>
        <View style={styles.infoRow}>
          <Text style={styles.infoLabel}>缩放级别:</Text>
          <Text style={styles.infoValue}>{zoomLevel.toFixed(1)}</Text>
        </View>
        <View style={styles.infoRow}>
          <Text style={styles.infoLabel}>标记数量:</Text>
          <Text style={styles.infoValue}>{markers.length}</Text>
        </View>
        {isRouteMode && (
          <View style={styles.infoRow}>
            <Text style={styles.infoLabel}>路线点数:</Text>
            <Text style={styles.infoValue}>{routePoints.length}</Text>
            {routePoints.length > 1 && (
              <TouchableOpacity onPress={clearRoute} style={styles.clearRouteButton}>
                <Text style={styles.clearRouteText}>清除路线</Text>
              </TouchableOpacity>
            )}
          </View>
        )}
      </View>

      {showMapTypeSelector && (
        <TouchableOpacity
          style={styles.modalOverlay}
          onPress={() => setShowMapTypeSelector(false)}
          activeOpacity={1}
        >
          <View style={styles.mapTypeSelector} onStartShouldSetResponder={() => true}>
            <Text style={styles.mapTypeTitle}>选择地图类型</Text>
            {mapTypes.map((type) => (
              <TouchableOpacity
                key={type.value}
                style={[
                  styles.mapTypeOption,
                  currentMapType === type.value && styles.mapTypeOptionActive,
                ]}
                onPress={() => toggleMapType(type.value)}
              >
                <Text
                  style={[
                    styles.mapTypeOptionText,
                    currentMapType === type.value && styles.mapTypeOptionTextActive,
                  ]}
                >
                  {type.label}
                </Text>
              </TouchableOpacity>
            ))}
          </View>
        </TouchableOpacity>
      )}

      {showSearchPanel && (
        <View style={styles.searchPanel}>
          <View style={styles.searchHeader}>
            <TextInput
              style={styles.searchInput}
              placeholder="搜索地点..."
              value={searchText}
              onChangeText={searchPOI}
              autoFocus
            />
            <TouchableOpacity
              style={styles.searchCloseButton}
              onPress={() => {
                setShowSearchPanel(false);
                setSearchText("");
              }}
            >
              <Text style={styles.searchCloseText}>取消</Text>
            </TouchableOpacity>
          </View>
          <FlatList
            data={filteredPOIs}
            keyExtractor={(item) => item.id}
            renderItem={({ item }) => (
              <TouchableOpacity
                style={styles.searchResultItem}
                onPress={() => {
                  selectSearchResult(item);
                  setShowSearchPanel(false);
                }}
              >
                <Text style={styles.searchResultName}>{item.name}</Text>
                <Text style={styles.searchResultCategory}>{item.category}</Text>
              </TouchableOpacity>
            )}
            ListEmptyComponent={
              <Text style={styles.searchEmpty}>未找到相关地点</Text>
            }
          />
        </View>
      )}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: "#F5F5F5" },
  header: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    padding: 12,
    backgroundColor: "#FFF",
    borderBottomWidth: 1,
    borderBottomColor: "#E5E5EA",
  },
  title: { fontSize: 18, fontWeight: "600", color: "#333" },
  headerButtons: { flexDirection: "row", gap: 8 },
  headerButton: {
    backgroundColor: "#007AFF",
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
  },
  headerButtonActive: { backgroundColor: "#FF9500" },
  headerButtonText: { color: "#FFF", fontSize: 14, fontWeight: "500" },
  map: { flex: 1 },
  mapControls: {
    position: "absolute",
    right: 16,
    bottom: 120,
    alignItems: "center",
    gap: 10,
  },
  zoomControls: {
    backgroundColor: "#FFF",
    borderRadius: 8,
    overflow: "hidden",
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.15,
    shadowRadius: 4,
    elevation: 4,
  },
  zoomButton: { width: 44, height: 44, justifyContent: "center", alignItems: "center" },
  zoomButtonText: { fontSize: 24, color: "#007AFF", fontWeight: "500" },
  zoomDivider: { height: 1, backgroundColor: "#E5E5EA" },
  controlButton: {
    backgroundColor: "#FFF",
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 8,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.15,
    shadowRadius: 4,
    elevation: 4,
  },
  controlButtonText: { color: "#007AFF", fontSize: 14, fontWeight: "500" },
  infoPanel: {
    backgroundColor: "#FFF",
    padding: 12,
    borderTopWidth: 1,
    borderTopColor: "#E5E5EA",
  },
  infoRow: { flexDirection: "row", alignItems: "center", marginBottom: 4 },
  infoLabel: { fontSize: 14, color: "#666", width: 80 },
  infoValue: { fontSize: 14, color: "#333", fontWeight: "500" },
  clearRouteButton: { marginLeft: 16, backgroundColor: "#FF3B30", paddingHorizontal: 10, paddingVertical: 4, borderRadius: 4 },
  clearRouteText: { color: "#FFF", fontSize: 12 },
  modalOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.4)", justifyContent: "center", alignItems: "center" },
  mapTypeSelector: {
    backgroundColor: "#FFF",
    borderRadius: 12,
    padding: 20,
    width: 280,
  },
  mapTypeTitle: { fontSize: 16, fontWeight: "600", marginBottom: 16, textAlign: "center" },
  mapTypeOption: {
    paddingVertical: 12,
    paddingHorizontal: 16,
    borderRadius: 8,
    marginBottom: 8,
    backgroundColor: "#F5F5F5",
  },
  mapTypeOptionActive: { backgroundColor: "#007AFF" },
  mapTypeOptionText: { fontSize: 16, color: "#333", textAlign: "center" },
  mapTypeOptionTextActive: { color: "#FFF" },
  searchPanel: {
    position: "absolute",
    top: 80,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: "#FFF",
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: -2 },
    shadowOpacity: 0.15,
    shadowRadius: 8,
    elevation: 8,
  },
  searchHeader: {
    flexDirection: "row",
    alignItems: "center",
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: "#E5E5EA",
  },
  searchInput: {
    flex: 1,
    height: 40,
    backgroundColor: "#F5F5F5",
    borderRadius: 8,
    paddingHorizontal: 12,
    fontSize: 16,
  },
  searchCloseButton: { marginLeft: 12, padding: 8 },
  searchCloseText: { color: "#007AFF", fontSize: 16 },
  searchResultItem: {
    padding: 16,
    borderBottomWidth: 1,
    borderBottomColor: "#E5E5EA",
  },
  searchResultName: { fontSize: 16, color: "#333", marginBottom: 4 },
  searchResultCategory: { fontSize: 14, color: "#666" },
  searchEmpty: { textAlign: "center", padding: 40, color: "#999", fontSize: 16 },
});

export default App;

⚠️ 遗留问题

  • AMapSdk.init() HarmonyOS 暂不支持,API Key 需在原生端配置
  • 热力图(HeatMap)部分功能未完全适配
  • 海量点(MultiPoint)性能优化中
  • 室内地图功能暂不支持
  • Cluster 组件存在类型定义问题

📚 参考资料

相关推荐
薛一半2 小时前
React组件通信初识
前端·react.js·前端框架
Mh17 小时前
react 设计哲学 | 严格模式
前端·react.js·preact
Jinuss21 小时前
源码分析之React中副作用Effect全流程
前端·javascript·react.js
Highcharts.js1 天前
在React中使用图表库时,优先选择组件化方案可以降低开发复杂度
前端·javascript·react.js·数据可视化·highcharts
西洼工作室1 天前
React城市选择模块功能实现
前端·react.js·前端框架
山科智能信息处理实验室1 天前
RENO:面向 3D LiDAR 点云的实时神经压缩
人工智能·3d
Highcharts.js1 天前
React如何集成图表?推荐使用Highcharts官方React封装库
javascript·react.js·前端框架·ecmascript
Yao.Li1 天前
基于 BOP 格式构建 PVN3D 自定义训练数据集技术文档
3d
sin°θ_陈1 天前
前馈式3D Gaussian Splatting 研究地图(路线三):大重建模型如何进入 3DGS——GRM、GS-LRM 与 Long-LRM 的方法转向
3d·aigc·gpu算力·三维重建·空间计算·3dgs·空间智能