1

在React Native App上之前使用的是通过Webview渲染一张Web地图(https://map.qq.com/api/gljs?v=1.exp&key=XXX),这么做的弊端就是速度慢而且不稳定,之前也用过高德地图,为了和微信小程序保持一致,需要用腾讯地图。

参考

腾讯地图(IOS)

实现的功能

  1. 地图中心点
  2. 缩放比例
  3. 地图控件(指南针,比例尺子)
  4. 多个标记点
  5. 根据多点设置最合适的视野
  6. 初次渲染完成回调
  7. 点击事件
  8. 主动移动地图后返回中心点坐标

申请apiKey

  1. 前往控制台
  2. 应用管理
  3. 我的应用
  4. 创建应用
  5. 要选中SDK

配置腾讯地图SDK

require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

pod 'AlipaySDK-iOS'
pod 'Tencent-MapSDK' <= 添加这个
...
cd ios && pod install

初始化地图服务

参考:配置开发秘钥隐私合规
创建TencentMap.hTencentMap.m

  1. TencentMap.h

#import <React/RCTViewManager.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <QMapKit/QMapKit.h>

@interface TencentMap : RCTViewManager
@end
  1. TencentMap.m
#import "TencentMap.h"
#import <React/RCTUIManager.h>
#import <React/UIView+React.h>
#import <QMapKit/QMSSearchKit.h>

@implementation TencentMap

RCT_EXPORT_MODULE();

// 初始化Key
RCT_EXPORT_METHOD(initQMap: (NSString *)key) {
    [QMapServices sharedServices].APIKey = key;
    [QMSSearchServices sharedServices].apiKey = key;
    if ([QMapServices sharedServices].APIKey.length == 0 || [QMSSearchServices sharedServices].apiKey.length == 0){
      NSLog(@"Fail");
    } else {
      NSLog(@"Success");
    }
    return;
}

// 同意隐私协议
RCT_EXPORT_METHOD(agreementQMap) {
    [[QMapServices sharedServices] setPrivacyAgreement:YES];
    return;
}


@end
  1. React Native 在我的App同意隐私协议用户协议之后调用
import {NativeModules} from 'react-native';

const TencentMap = NativeModules.TencentMap;

const initNativeQQMap = () => {
    if (TencentMap && TencentMap["initQMap"]) {
        TencentMap["initQMap"](KEY); <= 之前申请的apikey
        if (TencentMap["agreementQMap"]) {
            TencentMap["agreementQMap"]();
        }
    }
}

创建地图

创建TencentMapView.hTencentMapView.m以及工具TencentMapUtils.hTencentMapUtils.m

  1. TencentMapView.h
#import <UIKit/UIKit.h>
#import <QMapKit/QMapKit.h>
#import <React/RCTComponent.h>
#import <React/RCTViewManager.h>

@interface TencentMapView : UIView <QMapViewDelegate>

@property (nonatomic, strong) QMapView *mapView;
@property (nonatomic, assign) float zoomLevel; // 缩放级别
@property (nonatomic, assign) BOOL scaleViewFadeEnable; // 比例尺
@property (nonatomic, assign) BOOL showCompass; // 指南针
@property (nonatomic, strong) NSArray *coordinates; // 合适区域
@property (nonatomic, assign) CLLocationCoordinate2D mapCenter; // 中心点
@property (nonatomic, strong) NSArray *markers;  // 标记点
@property (nonatomic, strong) NSArray *linePoints;  // 线条点

@property (nonatomic, assign) BOOL isFirstRenderComplete; // 标记地图首次渲染是否完成
@property (nonatomic, strong) NSMutableArray<QPointAnnotation *> *markerAnnotations; // 多个标记点
@property (nonatomic, strong) NSMutableArray<QPolyline *> *markerLines; // 多个线条点


@property (nonatomic, copy) RCTBubblingEventBlock onClick;    // 点击事件回调
@property (nonatomic, copy) RCTBubblingEventBlock onMapLoad;    // 完成初次渲染
@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange; // 移动回调

@end

@interface TencentMapViewManager : RCTViewManager
@end

@interface CustomPointAnnotation : QPointAnnotation

@property (nonatomic, strong) NSDictionary *userInfo; // 存储额外数据

@end
  1. TencentMapView.m
#import "TencentMapView.h"
#import "TencentMapUtils.h"

@implementation CustomPointAnnotation

@end

@implementation TencentMapView

- (instancetype)init {
  self = [super init];
  if (self) {
    // 初始化地图视图
    self.mapView = [[QMapView alloc] initWithFrame:self.bounds];
    self.mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.mapView setMapType:QMapTypeStandard];
    [self.mapView setLogoScale:0.7];
    [self.mapView setZoomLevel:10];
    [self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(39.9042, 116.4074) animated:YES];
    self.mapView.delegate = self; // 设置代理
    
    [self addSubview:self.mapView];
    _isFirstRenderComplete = NO;
    _markerAnnotations = [NSMutableArray array];
    _markerLines = [NSMutableArray array];
  }
  return self;
}

- (void)layoutSubviews {
  [super layoutSubviews];
  self.mapView.frame = self.bounds; // 确保地图视图填充父视图
}

// props传参数
#pragma mark - Props

// 设置中心点
- (void)setMapCenter:(CLLocationCoordinate2D)mapCenter {
  if (!CLLocationCoordinate2DIsValid(mapCenter)) {
    return;
  }
  _mapCenter = mapCenter;
  [self.mapView setCenterCoordinate:mapCenter animated:YES];
}

// 设置是否显示指南针
- (void)setShowCompass:(BOOL)showCompass {
  _showCompass = showCompass;
  self.mapView.showsCompass = showCompass;
}

// 设置比例尺
- (void)setScaleViewFadeEnable:(BOOL)scaleViewFadeEnable{
  _scaleViewFadeEnable = scaleViewFadeEnable;
  self.mapView.showsScale = scaleViewFadeEnable;
}

// 设置地图缩放级别
- (void)setZoomLevel:(float)zoomLevel {
  _zoomLevel = zoomLevel;
  [self.mapView setZoomLevel:zoomLevel animated:YES];
}

// 计算边界矩形
- (void)setCoordinates:(NSArray *)coordinates {
  _coordinates = coordinates;
  if (coordinates.count > 0 && _isFirstRenderComplete) {
    [self adjustMapRegionToFitCoordinates];
  }
}

- (void) setLinePoints:(NSArray *)linePoints {
  _linePoints = linePoints;
  NSUInteger count = linePoints.count;
  if(count < 2){
    return; // 至少需要两个点才能绘制路线
  }
  if(_markerLines.count > 0){
    [self.mapView removeOverlays:_markerLines];
    [_markerLines removeAllObjects];
  }
  CLLocationCoordinate2D routeCoordinates[count];
  for (NSUInteger i = 0; i < count; i++) {
    NSDictionary *point = linePoints[i];
    routeCoordinates[i] = CLLocationCoordinate2DMake(
                                                     [point[@"latitude"] doubleValue],
                                                     [point[@"longitude"] doubleValue]
                                                     );
  }
  QPolyline *polyline = [QPolyline polylineWithCoordinates:routeCoordinates count:count];
  [self.mapView addOverlay:polyline];
  [_markerLines addObject:polyline];
}

- (void)setMarkers:(NSArray *)markers {
  _markers = markers;
  
  // 移除旧的标记点
  if (_markerAnnotations.count > 0) {
    [self.mapView removeAnnotations:_markerAnnotations];
    [_markerAnnotations removeAllObjects];
  }
  
  // 添加新的标记点
  for (NSDictionary *marker in markers) {
    NSDictionary *coordinateDict = marker[@"coordinate"]; // 图标位置
    NSDictionary *sizeDict = marker[@"size"]; // 图标大小
    NSDictionary *offsetDict = marker[@"offset"]; // 偏移
    NSString *icon = marker[@"icon"]; // 网络图标 URL
    if(!icon){
      icon = @"";
    }
    if(!sizeDict){
      if(icon.length){
        sizeDict = @{@"width": @40, @"height": @40};
      } else {
        sizeDict = @{@"width": @36, @"height": @51};
      }
    }
    if (!offsetDict){
      offsetDict = @{@"x": @0, @"y": @0};
    }
    CustomPointAnnotation *pointAnnotation = [[CustomPointAnnotation alloc] init];
    pointAnnotation.coordinate = CLLocationCoordinate2DMake([coordinateDict[@"latitude"] doubleValue], [coordinateDict[@"longitude"] doubleValue]);
    pointAnnotation.userInfo = @{ @"icon": icon, @"size": sizeDict, @"offset": offsetDict, };
    // 将点标记添加到地图中
    [self.mapView addAnnotation:pointAnnotation];
    [_markerAnnotations addObject:pointAnnotation];
  }
}

#pragma mark - Helper Methods

// 计算边界矩形并调整地图区域
- (void)adjustMapRegionToFitCoordinates {
  NSUInteger count = self.coordinates.count;
  if (count == 0) {
    return;
  }
  CLLocationCoordinate2D coordinates[count];
  for (NSUInteger i = 0; i < count; i++) {
    NSDictionary *coordinate = self.coordinates[i];
    CLLocationDegrees lat = [coordinate[@"latitude"] doubleValue];
    CLLocationDegrees lng = [coordinate[@"longitude"] doubleValue];
    coordinates[i] = CLLocationCoordinate2DMake(lat, lng);
  }
  QCoordinateRegion region = QBoundingCoordinateRegionWithCoordinates(coordinates, count);
  [self.mapView setRegion:region edgePadding:UIEdgeInsetsMake(30, 30, 30, 30) animated:YES];
}



#pragma mark - QMapViewDelegate

- (void)mapView:(QMapView *)mapView regionDidChangeAnimated:(BOOL)animated gesture:(BOOL)bGesture {
  if (bGesture) {
    // 获取地图视图的中心点坐标
    CLLocationCoordinate2D centerCoordinate = mapView.centerCoordinate;
    // 触发事件,将中心点坐标传递给 React Native
    if (self.onRegionChange) {
      self.onRegionChange(@{
        @"latitude": @(centerCoordinate.latitude),
        @"longitude": @(centerCoordinate.longitude)
      });
    }
  }
}

- (void)mapView:(QMapView *)mapView didTapAtCoordinate:(CLLocationCoordinate2D)coordinate {
  // 地图点击事件
  if (self.onClick) {
    self.onClick(@{
      @"latitude": @(coordinate.latitude), // 纬度
      @"longitude": @(coordinate.longitude) // 经度
    });
  }
}

- (void)mapViewFirstRenderDidComplete:(QMapView *)mapView {
  // 地图首次渲染完成后调用
  _isFirstRenderComplete = YES;
  if (self.coordinates.count > 0) {
    [self adjustMapRegionToFitCoordinates];
  }
  if (self.onMapLoad){
    self.onMapLoad(@{});
  }
}

- (QOverlayView *)mapView:(QMapView *)mapView viewForOverlay:(id<QOverlay>)overlay {
  if ([overlay isKindOfClass:[QPolyline class]]) {
    QPolylineView *polylineView = [[QPolylineView alloc] initWithPolyline:overlay];
    polylineView.strokeColor = [UIColor colorWithRed:118/255.0 green:192/255.0 blue:111/255.0 alpha:1.0]; // 设置路线颜色
    polylineView.lineWidth = 6; // 设置路线宽度
    polylineView.borderWidth = 1;
    polylineView.borderColor = [UIColor whiteColor];
    return polylineView;
  }
  return nil;
}

- (QAnnotationView *)mapView:(QMapView *)mapView viewForAnnotation:(id<QAnnotation>)annotation {
  if ([annotation isKindOfClass:[CustomPointAnnotation class]]) {
    static NSString *annotationIdentifier = @"pointAnnotation";
    QPinAnnotationView *pinView = (QPinAnnotationView *)[self.mapView dequeueReusableAnnotationViewWithIdentifier:annotationIdentifier];
    if (pinView == nil) {
      pinView = [[QPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:annotationIdentifier];
      pinView.canShowCallout = NO;
      NSString *icon = ((CustomPointAnnotation *)annotation).userInfo[@"icon"];
      NSDictionary* size = ((CustomPointAnnotation *)annotation).userInfo[@"size"];
      NSDictionary* offset = ((CustomPointAnnotation *)annotation).userInfo[@"offset"];
      
      CGFloat width = [size[@"width"] doubleValue];
      CGFloat height = [size[@"height"] doubleValue];
      CGFloat offsetX = [offset[@"x"] doubleValue];
      CGFloat offsetY = [offset[@"y"] doubleValue];
      // 设置偏移(没法准确)
      // pinView.centerOffset  = CGPointMake(offsetX, offsetY);
      // 默认图标marker添加到Images.xcassets
      if (icon.length > 0) {
        [TencentMapUtils loadImageFromURL:icon completion:^(UIImage *image) {
          if (image) {
            CGSize newSize = CGSizeMake(width, height);
            UIImage *resizedImage = [TencentMapUtils resizeImage:image toSize:newSize];
            pinView.image = resizedImage;
          } else {
            // 设置默认marker大小
            UIImage *image = [UIImage imageNamed:@"marker"];
            CGSize newSize = CGSizeMake(width, height);
            UIImage *resizedImage = [TencentMapUtils resizeImage:image toSize:newSize];
            pinView.image = resizedImage;
          }
        }];
      } else {
        // 设置默认marker大小
        UIImage *image = [UIImage imageNamed:@"marker"];
        CGSize newSize = CGSizeMake(width, height);
        UIImage *resizedImage = [TencentMapUtils resizeImage:image toSize:newSize];
        pinView.image = resizedImage;
      }
    }
    return pinView;
  }
  
  return nil;
}

@end

// 导出地图组件
@implementation TencentMapViewManager

RCT_EXPORT_MODULE(TencentMapView)

- (UIView *)view {
  return [[TencentMapView alloc] init];
}

RCT_EXPORT_VIEW_PROPERTY(mapCenter, CLLocationCoordinate2D)
RCT_EXPORT_VIEW_PROPERTY(zoomLevel, float)
RCT_EXPORT_VIEW_PROPERTY(scaleViewFadeEnable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showCompass, BOOL)
RCT_EXPORT_VIEW_PROPERTY(coordinates, NSArray)
RCT_EXPORT_VIEW_PROPERTY(markers, NSArray)
RCT_EXPORT_VIEW_PROPERTY(linePoints, NSArray)
RCT_EXPORT_VIEW_PROPERTY(onClick, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMapLoad, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)

@end
  1. TencentMapUtils.h
#import <UIKit/UIKit.h>

@interface TencentMapUtils : NSObject

// 调整图片大小
+ (UIImage *)resizeImage:(UIImage *)image toSize:(CGSize)size;

// 从 URL 加载图片
+ (void)loadImageFromURL:(NSString *)urlString completion:(void (^)(UIImage *))completion;

@end
  1. TencentMapUtils.m
#import "TencentMapUtils.h"
#import <Foundation/Foundation.h>

@implementation TencentMapUtils

// 调整图片大小
+ (UIImage *)resizeImage:(UIImage *)image toSize:(CGSize)size {
    UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
    [image drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return resizedImage;
}

// 从 URL 加载图片
+ (void)loadImageFromURL:(NSString *)urlString completion:(void (^)(UIImage *))completion {
    NSURL *url = [NSURL URLWithString:urlString];
    if (!url) {
        completion(nil);
        return;
    }

    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (data && !error) {
            UIImage *image = [UIImage imageWithData:data];
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(image);
            });
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion(nil);
            });
        }
    }];
    [task resume];
}


@end

React Native 使用

import React from 'react';
import {requireNativeComponent, StyleSheet, ViewStyle} from 'react-native';

const TencentMapView = requireNativeComponent('TencentMapView');


interface Marker {
    // 标记点位置
    coordinate: {
        latitude: number;
        longitude: number;
    };
    // 标记点位置
    size?: {
        width: number;
        height: number;
    };
    // 标记点网络图片
    icon?: string;
    // 偏移度
    // offset?: {
    //     x: number;
    //     y: number;
    // }
}

interface Location {
    latitude: number; 
    longitude: number;
}

interface Props {
    // 比例尺
    scaleViewFadeEnable?: boolean;
    // 样式
    style?: ViewStyle;
    // 比例
    zoom?: number;
    // 中心点经纬度
    lng?: number;
    lat?: number;
    // 指南针
    showCompass?: boolean;
    // 标记点
    markers?: Marker[];
    // 点击事件
    onClick?: (e: Location) => void;
    // 移后事件
    onRegionChange?: (e: Location) => void;
    // 初次完成
    onLoad?: () => void;
    // 安全区域
    coordinates?: Location[];
    // 线条
    linePoints?: Location[];
}

const NativeMapComponent: React.FC<Props> = (props) => {
    const defaultPoint = React.useRef({latitude: 39.9042, longitude: 116.4074}).current;
    const {style, zoom, lat, lng, scaleViewFadeEnable, showCompass, markers, coordinates, linePoints, onClick, onLoad, onRouteDistance, onRegionChange} = props;
    
    const checkLocation = React.useCallback((lat, lng) => {
        if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
            return {latitude: lat, longitude: lng};
        }
        return defaultPoint;
    }, [defaultPoint]);
    
    const center = React.useMemo(() => {
        if (typeof lng !== "undefined" && typeof lat !== "undefined") {
            const latitude = Number(lat);
            const longitude = Number(lng);
            return checkLocation(latitude, longitude);
        }
        return defaultPoint;
    }, [lat, lng, defaultPoint, checkLocation]);
    
    
    return <TencentMapView
        linePoints={linePoints || []}
        scaleViewFadeEnable={scaleViewFadeEnable || false}
        zoomLevel={zoom || 10}
        style={StyleSheet.flatten([styles.container, StyleSheet.flatten(style)])}
        mapCenter={center}
        showCompass={showCompass || false}
        markers={markers || []}
        coordinates={coordinates || []}
        onRegionChange={onRegionChange ? (event) => {
            if (onRegionChange) {
                const {latitude, longitude} = event.nativeEvent;
                onRegionChange({latitude, longitude});
            }
        } : undefined}
        onClick={onClick ? (event) => {
            if (onClick) {
                const {latitude, longitude} = event.nativeEvent;
                onClick({latitude, longitude});
            }
        } : undefined}
        onMapLoad={onLoad ? () => {
            if (onLoad) {
                onLoad();
            }
        } : undefined}
    /> ;
};

const styles = StyleSheet.create({
    container: {
        width: "100%",
        height: "100%",
        backgroundColor: "#f2f1ee"
    },
});

export default NativeMapComponent;

使用

  1. 基础地图
<NativeMapComponent
    onRegionChange={(e) => {
        
    }}
    onClick={(e) => {
        
    }}
    markers={[{
        coordinate: {
            longitude: 120.450178,
            latitude: 29.054048,
        },
    }]}
    showCompass={true}
    scaleViewFadeEnable={true}
    zoom={10}
    lng={120.450178}
    lat={29.054048}
    style={{
        width: "100%",
        height: "100%",
    }}
/>

  1. 自定义图标
<NativeMapComponent
    onRegionChange={(e) => {
        
    }}
    onClick={(e) => {
        
    }}
    markers={[{
        icon: "https://xxxEnd.png",
        coordinate: {
            longitude: 120.450178,
            latitude: 29.054048,
        },
        size: {
            width: 21,
            height: 52
        },
    }]}
    // showCompass={true}
    // scaleViewFadeEnable={true}
    zoom={10}
    lng={120.450178}
    lat={29.054048}
    style={{
        width: "100%",
        height: "100%",
    }}
/>

  1. 线路图 (调整合适区域)
<NativeMapComponent
    linePoints={linePoints}
    markers={[
        {
            coordinate: {latitude: 39.9042, longitude: 116.4074},
            icon: "https://xxxEnd.png",
            size: {width: 21, height: 52}
        },
        {
            coordinate: {latitude: 31.2304, longitude: 121.4737},
            icon: "https://xxxgreenMark.png",
            size: {width: 14, height: 14}
        },
        {
            coordinate: {latitude: 23.1291, longitude: 113.2644},
            icon: "https://xxxStart.png",
            size: {width: 21, height: 52}
        },
    ]}
    coordinates={[
        {latitude: 39.9042, longitude: 116.4074},
        {latitude: 31.2304, longitude: 121.4737},
        {latitude: 23.1291, longitude: 113.2644},
    ]}
/>

对比Web地图

第一张是web地图,第二张是IOS原生地图

完整代码

代码


perkz
51 声望28 粉丝