最近有个React-native项目需要添加定位功能,看了网上很多相关得帖子,基本都是桥接的3方的SDK ,而且有些功能不能定制,考虑到以后百度SDK版本更新的问题,以及方便后期维护,决定自己桥接一下百度原生定位功能
1.React-Native桥接Android原生UI 组件和原生模块
具体步骤
- 创建一个继承React-Native ViewGroupManager 的 java 类
//MapViewManager.java
public class MapViewManager extends ViewGroupManager<MapView> {
}
- 重写 getName 方法,返回一个字符串,对应JavaScript 中requireNativeComponent 的对象
//MapViewManager.java
@Override
public String getName() {
return "MSMapView";
}
- 重写 createViewInstance 方法,返回自定义的原生组件
//MapViewManager.java
@Override
protected MapView createViewInstance(ThemedReactContext themedReactContext) {
//创建自定义的MapView
}
- 导出视图的属性设置器:通过@ReactProp 注解
//MapViewManager.java
@ReactProp(name = "posLatitude")
public void setPosLatitude(MapView mapView, double posLatitude) {
mStopLatitude = posLatitude;
}
- 对应JavaScript 模块调用
//MapView.js
MapView.propTypes = {
posLatitude: PropTypes.number.isRequired
}
- 把视图管理类注册到应用程序包的creatViewManagers里
//MapViewPackage.java
public class MapViewPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new GeolocationModule(reactContext));
return modules;
}
- 在JavaScript 中使用
//MapView.js
import React from 'react'
import { requireNativeComponent } from 'react-native'
import PropTypes from 'prop-types'
const MSMapView = requireNativeComponent('MSMapView', null)
export default function MapView(props) {
return <MSMapView {...props} />
}
MapView.propTypes = {
posLatitude: PropTypes.number.isRequired,
posLongitude: PropTypes.number.isRequired,
posName: PropTypes.string.isRequired,
posAddress: PropTypes.string.isRequired
}
具体参考官方文档原生UI组件
-
1.2 桥接原生模块
- 首先来创建一个原生模块(GeolocationModule) 。一个原生模块是一个继承了ReactContextBaseJavaModule的Java类,它可以实现一些JavaScript所需的功能
//GeolocationModule.java
public class GeolocationModule extends ReactContextBaseJavaModule {
public GeolocationModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}
- 实现getName方法。这个函数用于返回一个字符串名字,这个名字在JavaScript端标记这个模块
//GeolocationModule.java
public String getName() {
return "MSLocationManager";
}
- 导出一个方法给JavaScript使用,Java方法需要使用注解@ReactMethod。方法的返回类型必须为void
//GeolocationModule.java
@ReactMethod
public void startLocationWithCallback(Callback locationCallback) {
//添加回调
Logger.t(TAG).i("startLocationWithCallback");
this.locationCallback = locationCallback;
initLocationClient();
}
- 注册模块,在应用的Package类的createNativeModules方法中添加这个模块
//MapViewPackage .java
public class MapViewPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new GeolocationModule(reactContext));
return modules;
}
}
- 对应JavaScript 模块调用
//JS
import { NativeModules } from 'react-native'
const LocationManager = NativeModules.MSLocationManager
LocationManager.startLocationWithCallback((error, location) => {
if (error) {
logger.error(`get location error:${JSON.stringify(error)}`)
...
} else {
...
}
})
2.Android 百度地图SDK配置
注意应用混淆
集成地图SDK的应用,在打包混淆的时候,需要注意与地图SDK相关的方法不可被混淆。混淆方法如下:
-keep class com.baidu.** {*;}
-keep class vi.com.** {*;}
-dontwarn com.baidu.**
保证百度类不能被混淆,否则会出现网络不可用等运行时异常
<!-- 获取运营商信息,用于支持提供运营商信息相关的接口 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 访问网络,网络定位需要上网 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 用于读取手机当前的状态 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 这个权限用于进行网络定位 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 这个权限用于访问GPS定位 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 写入扩展存储,向扩展卡写入数据,用于写入离线定位数据 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<service
android:name="com.baidu.location.f"
android:enabled="true"
android:process=":remote" />
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="你申请的AK码" />
android {
.....
defaultConfig {
.....
ndk {
// 设置支持的SO库架构
abiFilters 'armeabi' , 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
.....
}
sourceSets {
main {
jniLibs.srcDir 'libs'
jni.srcDirs = []
}
}
.....
}
3.MapView 加载
- 3.1 根据React-Native桥接原生UI(前面已经讲过,具体参考步骤1.1),需要在createViewInstance 创建 View(BaiduMapView) ,代码如下
//MapViewPackage.java
@Override
protected MapView createViewInstance(ThemedReactContext themedReactContext) {
//在使用SDK各组件之前初始化context信息,传入ApplicationContext
SDKInitializer.initialize(themedReactContext.getApplicationContext());
//创建MapView对象
mapView = new MapView(themedReactContext);
mIconMarker = BitmapDescriptorFactory.fromResource(R.drawable.map_maker);
this.themedReactContext = themedReactContext;
initMap();//初始化mapView参数
return mapView;
}
具体参考创建BaiduView文档
- 3.2 在 MapView 上添加 Location(定位) 和 InfoWindow 布局,注意要在 MapLoaded后添加
//MapViewPackage.java
private void initMap() {
mBaiduMap = mapView.getMap();
mBaiduMap.setMapType(BaiduMap.MAP_TYPE_NORMAL);
mapView.showZoomControls(true);
//定义BaiduMapOptions对象
BaiduMapOptions options = new BaiduMapOptions();
options.scaleControlEnabled(false); // 不显示比例尺
mBaiduMap.setMyLocationEnabled(true);
mBaiduMap.setMaxAndMinZoomLevel(18, 7);//地图的最大最小缩放比例7-18
mBaiduMap.setOnMapLoadedCallback(new BaiduMap.OnMapLoadedCallback() {
@Override
public void onMapLoaded() {
setLocation();
setInfoWindow();
}
});
mBaiduMap.setOnMapClickListener(new BaiduMap.OnMapClickListener() {
@Override
public boolean onMapPoiClick(MapPoi arg0) {
return false;
}
@Override
public void onMapClick(LatLng arg0) {
mBaiduMap.hideInfoWindow();
}
});
}
根据已经获取的经纬度,设置定位布局
//MapViewPackage.java
private void setLocation() {
//获取定位
LatLng latLng = new LatLng(mStopLatitude, mStopLongitude);
//添加定位图标
OverlayOptions overlayOptions = new MarkerOptions().position(latLng)
.icon(mIconMarker).zIndex(3);
Marker marker = (Marker) mBaiduMap.addOverlay(overlayOptions);
Bundle bundle = new Bundle();
bundle.putDoubleArray("location", new double[]{mStopLatitude, mStopLongitude});
marker.setExtraInfo(bundle);
MapStatus.Builder builder = new MapStatus.Builder();//地图状态构造器
//设置地图中心点,设置地图缩放级别并创建地图状态对象
MapStatus mapStatus = builder.target(latLng).zoom(14f).build()
MapStatusUpdate mapStatusUpdate = MapStatusUpdateFactory.newMapStatus(mapStatus);
//以动画方式更新地图状态,动画耗时 300 ms
mBaiduMap.animateMapStatus(mapStatusUpdate);
}
设置 InfoWindow 布局,并监听布局点击状态
//MapViewPackage.java
void setInfoWindow() {
Logger.t(TAG).i("setInfoWindow");
//显示信息窗
mBaiduMap.setOnMarkerClickListener(new BaiduMap.OnMarkerClickListener() {
InfoWindow.OnInfoWindowClickListener infoWindow_ClickListener = new InfoWindow.OnInfoWindowClickListener() {
@Override
public void onInfoWindowClick() { // TODO Auto-generated method stub
mBaiduMap.hideInfoWindow();
}
};
@Override
public boolean onMarkerClick(Marker marker) {
LatLng point = new LatLng(mStopLatitude, mStopLongitude);
View infoWindowView = LayoutInflater.from(themedReactContext).inflate(R.layout.info_window, null);
TextView posName = infoWindowView.findViewById(R.id.infoWindow_posName);
TextView posAddress = infoWindowView.findViewById(R.id.infoWindow_posAddress);
posName.setText(mPosName);
posAddress.setText(mPosAddress);
//根据一个 View 创建 Bitmap 描述信息, 当 view 为 null 时返回 null
BitmapDescriptor infoWindow_bitmap = BitmapDescriptorFactory.fromView(infoWindowView);
mInfoWindow = new InfoWindow(infoWindow_bitmap, point, -marker.getIcon().getBitmap().getHeight()/2, infoWindow_ClickListener);
//显示信息窗
Logger.t(TAG).i("showInfoWindow");
mBaiduMap.showInfoWindow(mInfoWindow);
return true;
}
});
}
infowwindow.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/popup"
android:paddingTop="@dimen/map_info_window_padding_top"
android:paddingBottom="@dimen/map_info_window_padding_bottom"
android:paddingLeft="@dimen/map_info_window_padding_left"
android:paddingRight="@dimen/map_info_window_padding_right"
android:orientation="vertical">
<TextView
android:id="@+id/infoWindow_posName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal" />
<TextView
android:id="@+id/infoWindow_posAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/map_info_window_pos_name_text_size"/>
</LinearLayout>
- 3.3 桥接Android 原生模块(前面已经讲过,参考1.2),获取经纬度定位信息,通过 LocationClient 发起定位,通过 LocationClientOption 设置LocationClient相关参数
//GeolocationModule.java
private void initLocationClient() {
LocationClientOption option = new LocationClientOption();//声明LocationClient类实例并配置定位参数
option.setLocationMode(LocationMode.Hight_Accuracy);//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
option.setCoorType("bd09ll");//可选,默认gcj02,设置返回的定位结果坐标系
option.setIsNeedAddress(true);//可选,设置是否需要地址信息,默认不需要
option.setIsNeedAltitude(true);//可选,默认false,设置定位时是否需要海拔信息,默认不需要,除基础定位版本都可用
option.setIsNeedLocationDescribe(true);//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
option.setOpenGps(true);//可选,默认false,设置是否使用gps
option.setScanSpan(1000);//可选,默认0,即仅定位一次,设置发起连续定位请求的间隔需要大于等于1000ms才是有效的
//定位初始化
locationClient = new LocationClient(getReactApplicationContext()/*context.getApplicationContext()*/);
locationClient.setLocOption(option);
//开启地图定位图层
locationClient.start();
Log.i("locationClient", "locationClient");
//注册LocationListener监听器
MyLocationListener myLocationListener = new MyLocationListener();
locationClient.registerLocationListener(myListener);
}
public class MyLocationListener extends BDAbstractLocationListener {
private double latitude, longitude;
/**
* 实现定位监听 位置一旦有所改变就会调用这个方法
* 可以在这个方法里面获取到定位之后获取到的一系列数据
*/
@Override
public void onReceiveLocation(BDLocation bdLocation) {
latitude = bdLocation.getLatitude();
longitude = bdLocation.getLongitude();
Logger.t(TAG).i("onReceiveLocation");
if (bdLocation != null) {
StringBuffer buffer = new StringBuffer(256);
buffer.append(latitude);
buffer.append(",");
buffer.append(longitude);
location = buffer.toString();
locationClient.stop();
try {
GeolocationModule.this.locationCallback.invoke(null, location);
} catch (IllegalViewOperationException e) {
locationCallback.invoke(e.getMessage(), null);
}
}
Logger.t(TAG).i("onReceiveLocation:" + location
+ "latitude:" + latitude
+ "longitude:" + longitude);
}
}