在原生的开发中使用高德或者百度地图实现定位等相关功能是每一个小伙伴必备的技能,那么现在我要在RN中实现定位的功能该怎么做呢?一看到需要实现这样的功能,第一反应就是打开高德开放平台看看是否有相关的SDK,可是翻来翻去也只看到了Android原生的实现方式。但是,RN可以实现与原生的混合开发,那么是不是可以尝试下在RN页面调用原生里面的定位功能呢?
尝试开始(内心还是有点小激动...):
1.在Android工程中完成相关配置
此步骤跟原生开发没有任何的区别,可以参照高德开放平台,此处不再介绍。
2.创建原生模块
原生模块是一个继承于
ReactContextBaseJavaModule
的Java类
创建一个继承ReactContextBaseJavaModule
的Java类,并在此类中实现定位功能,先上代码:
public class AMapLocationModule extends ReactContextBaseJavaModule {
private ReactApplicationContext mReactApplicationContext;
private AMapLocationClient mLocationClient = null;
// 定位回调监听器
private AMapLocationListener mAMapLocationListener = new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation aMapLocation) {
// 实例化一个回调给RN端的map对象
WritableMap params = Arguments.createMap();
if (aMapLocation != null) {
if (aMapLocation.getErrorCode() == 0) {
// 定位成功
params.putString("address", aMapLocation.getAddress());
} else {
// 定位失败
params.putBoolean("result", false);
}
}
// 发送给RN端
sendEvent("onLocationChanged", params);
}
};
private void sendEvent(String eventName, @Nullable WritableMap params) {
if (mReactApplicationContext != null) {
mReactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
}
public AMapLocationModule(ReactApplicationContext reactContext) {
super(reactContext);
// 在构造函数中实例化定位功能的相关对象,并设置相关的定位配置,跟原生实现定位功能一样配置
mReactApplicationContext = reactContext;
mLocationClient = new AMapLocationClient(reactContext);
mLocationClient.setLocationListener(mAMapLocationListener);
AMapLocationClientOption clientOption = new AMapLocationClientOption();
clientOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Battery_Saving); // 低功耗模式, 只使用wifi
clientOption.setOnceLocation(true);
mLocationClient.setLocationOption(clientOption);
}
@Override
public String getName() {
return "AMapLocation"; //此Name在RN端标记这个模块,不能与已有的Name发生冲突
}
@ReactMethod // 可以被RN端调用的方法
public void destory() {
if (mLocationClient != null) {
mLocationClient.stopLocation(); // 停止定位
mLocationClient.onDestroy(); // 销毁定位
}
}
@ReactMethod // 可以被RN端掉用的方法,开启定位
public void startLocation() {
if (mLocationClient != null) {
mLocationClient.startLocation();
}
}
}
在上述的代码中,必须注意以下几个点:
-
getName()
方法
这是ReactContextBaseJavaModule
的派生类必须实现的方法。只要返回一个字符串名称,在RN端才能找到这个模块,需要防止命名冲突。还有一点,如果名称中有RCT作为前缀,会被自动忽略移除的,比如RCTAMapLocation
,那么在RN端通过React.NativeModules.AMapLocation
就能访问这个模块了。 -
ReactMethod
注解
只有使用了此注解的public
void
方法才能被RN端访问,RN端可以通过此方法的参数向原生端传递数据。上面代码中,RN端能够访问startLocation()
方法以及destory()
方法。还有一点需要注意的是,RN端访问原生是异步进行的。 -
sendEvent ()
方法
只要原生中的方法通过ReactMethod
注解,RN端就能够访问得到,那么我们要在RN端启动定位服务这个功能应该已经得到了实现,那么在RN端如何监听定位的结果呢?如何将定位的结果传递给RN端呢?其实通过sendEvent()
方法就能够得到实现。
在这肯定会有一个疑问,为什么仅仅通过这么一个方法就能够让RN端监听并接收原生端的数据呢?
其实实现这个功能还是需要借助RCTDeviceEventEmitter
这个玩意的。通过代码中的sendEvent()
方法可以看到,通过reactContext
来获得RCTDeviceEventEmitter
的引用,然后调用RCTDeviceEventEmitter
中的emit()
方法。这样在RN端就可以添加监听回调事件。 -
WritableMap()
方法
顾名思义这是一个可写的map类,通过它,我们可以通过键值对的方式将需要发送给RN端的信息封装起来,并通过emit ()
方法发送出去。其实还有一个ReadableMap用来存放RN传递给原生的数据。
3. 注册模块
在第二个步骤中,我们将可以被RN调用的原生模块给创建好了,为了让RN端可以识别此模块,就少不了注册这一个步骤。
注册模块相对简单一点,创建一个实现ReactPackage
接口的Java类,实现相应的方法就OK了,下面看下代码:
public class AMapLocationReactPackage implements ReactPackage {
@Override
public List createNativeModules(ReactApplicationContext reactContext) {
List list = new ArrayList<>();
list.add(new AMapLocationModule(reactContext));
return list;
}
@Override
public List> createJSModules() {
return Collections.emptyList();
}
@Override
public List createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
最后需要记住在Application里面ReactNativeHost
的getPackages()
方法里面添加上AMapLocationReactPackage
实例。
在AMapLocationReactPackage
这个类中,需要实现三个方法:
-
createNativeModules()
方法
此方法是向ReactPackage
中添加可以被RN端调用的原生模块。在第二个步骤中创建AMapLocationModule
模块就是为了让RN端调用的,所以需要在此方法进行注册。 -
createJSModules ()
方法
此方法是添加可以让原生调用的Js模块,这边没有此需求。所以就传递个空集合给ReactPackage
。 -
createViewManagers()
方法
此方法是添加可以让RN端调用的原生View组件,比如项目中原本就有一个写的很nice的原生自定义View,不想在RN端重新去写,就可以通过实现一个ViewManager
派生类,并通过createViewManagers
注册,就能在RN进行调用。
到这Android原生端的代码就告一段落了,来个中场休息吧(啦啦队该上场...),接下来实现下RN端的代码就能够知道我这个尝试是否能够成功了。
4.在RN端获取模块
还记得在创建模块的时候讲过,getName()
方法返回了一个字符串,那么在RN端通过此字符串就能获得相对应的模块的引用了。所以此例子中,在RN端通过NativeModules.AMapLocation
就能获取刚刚创建的模块实例,这意味着现在已经可以调用模块中通过ReactMethod
注解的方法了,为了更好的使用,我做了个简单的封装。
import {NativeModules, DeviceEventEmitter} from 'react-native';
const location = NativeModules.AMapLocation;
export default class AMapLocation {
// 开启定位
static startLocation() {
location.startLocation();
}
// 关闭定位
static destory() {
location.destory();
}
// 注册定位回调监听
static addEventListener(handler) {
const listener = DeviceEventEmitter.addListener(
'onLocationChanged', // 与模块中的eventName保持一致
handler // 回调函数,在此函数接收定位信息
);
return listener;
}
}
-
addEventListener()
方法
这是一个注册回调监听方法,也就是为了接收从原生端传递过来的定位信息。在原生端通过RCTDeviceEventEmitter
的emit()
方法来发送事件的,RN端同样是通过DeviceEventEmitter (模块名的RCT前缀会被自动移除)
来添加监听事件。需要传递一个与原生相同的eventName字符串以及一个回调方法。
5.在RN端开启定位服务并接收定位数据
接下来就在RN端开启个定位服务,看看能不能成功开启并且接受到当前的地址信息。
import AMapLocation from '../modules/AMapLocation';
componentDidMount() {
this.listener = AMapLocation.addEventListener(this._onLocationChanged); // 注册监听
AMapLocation.startLocation(); // 开启定位
}
componentWillUnmount() {
AMapLocation.destory();
this.listener.clear();
}
_onLocationChanged = (data)=> {
if (data && data.result) {
console.log(data.address);
}
};
很常规的做法,在componentDidMount ()
方法中注册监听并开启定位;在componentWillUnmount()
方法中关闭定位,清除监听事件;
_onLocationChanged()
方法中接收数据。
6.验证结果(鸡冻ing...)
历经千辛万苦终于到了这一步了,那么就来看下log吧。
地址信息从logo中显示出来了,也就意味着这次尝试成功了!
6.总结
通过此次尝试差不多也就学会了RN与原生的一种混合开发方式,那就来让我们回忆一下混合开发大致的步骤吧。
- 在Android端继承
ReactContextBaseJavaModule
创建一个原生模块,并在原生模块中通过ReactMethod
注解暴露出可以让RN端调用的方法,并在getName()
方法中返回一个模块名称字符串; - 在模块中通过
RCTDeviceEventEmitter
的emit()
方法实现向RN端发送事件; - 通过实现
ReactPackage
接口注册模块,需要区别三个createXXX()
方法对应的使用方式,同时不要忘记在Application中添加package实例; - 在RN端可以利用对应的模块名称调用暴露的方法,同时可以通过
DeviceEventEmitter
注册回调的监听事件; - 需要记住RN中调用原生的方法都是异步进行的;
- 混合开发中还可以通过
Promises
来实现回调函数。
通过原生向RN发送事件的方法实现了高德地图的定位功能,这是一次成功的尝试,但这才迈出了第一步。。。