在React Native App上之前使用的是通過Webview渲染一張Web地圖(https://map.qq.com/api/gljs?v=1.exp&key=XXX),這麼做的弊端就是速度慢而且不穩定,之前也用過高德地圖,為了和微信小程序保持一致,需要用騰訊地圖。
參考
騰訊地圖(IOS)
實現的功能
- 地圖中心點
- 縮放比例
- 地圖控件(指南針,比例尺子)
- 多個標記點
- 根據多點設置最合適的視野
- 初次渲染完成回調
- 點擊事件
- 主動移動地圖後返回中心點座標
申請apiKey
- 前往控制枱
- 應用管理
- 我的應用
- 創建應用
- 要選中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.h和TencentMap.m
- TencentMap.h
#import <React/RCTViewManager.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <QMapKit/QMapKit.h>
@interface TencentMap : RCTViewManager
@end
- 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
- 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.h和TencentMapView.m以及工具TencentMapUtils.h和TencentMapUtils.m
- 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
- 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
- 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
- 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;
使用
- 基礎地圖
<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%",
}}
/>
- 自定義圖標
<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%",
}}
/>
- 線路圖 (調整合適區域)
<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原生地圖
完整代碼
代碼