React Native自定义原生控件(Android)
React Native是将原生控件封装桥接成JS组件来使用的,这保证了其性能的高效性。官方已经为开发者封装了很多常用的组件,如ScrollView,TextInput,FlatList等。但开发中你可能想自己将之前封装的一些原生组件桥接到RN中来使用,下面就讨论下如何封装一个原生组件到RN端使用。
关羽RN的桥接基本上有两种:
- Native Modules
- Native UI Components
Native Modules是RN将某些功能桥接到原生来操作,比如操作和读取传感器数值等,比较简单。下面着重讨论下Native UI Components,即让Javascript可以使用原生UI组件。下面通过一个例子来说明这个过程,我们在原生实现了一个圆形的ImageView,现在想把它桥接到Javascript中使用。
1. 实现ViewManager子类
实现的ViewManager的子类负责原生View创建和管理。SimpleViewManager是ViewManager的一个子类,继承它可以更方便的管理View,因为它已经包含更多公共的属性,如背景颜色、透明度、Flexbox 布局等。
//ReactCircleImageManager.java
package com.rnvc.widget.image;
...
@ReactModule(name = ReactCircleImageManager.REACT_CLASS)
public class ReactCircleImageManager extends SimpleViewManager<CircleImageView> {
protected static final String REACT_CLASS = "RCTCircleImage";
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected CircleImageView createViewInstance(final ThemedReactContext reactContext) {
final CircleImageView imageView = new CircleImageView(reactContext);
return imageView;
}
}
在ReactCircleImageManager类中有两个重要方法,getName方法返回该View的的唯一索引,在JS中就是根据这个名字来找到相应的原生组件的;createViewInstance方法中生成原生CircleImageView的实例。
2. 生成PackageModule并注册ViewManager
PackageModule是用于注册Native Modules和Native UI Components。
//CusReactPackage.java
package com.rnvc.rnmodule;
...
public class CusReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.<ViewManager>singletonList(
new ReactCircleImageManager()
);
}
}
其中createNativeModules方法用户注册Native Modules,createViewManagers用于注册Native UI Components。
package com.rnvc.rnmodule;
...
public class YDReactNativeHost extends ReactNativeHost {
public YDReactNativeHost(Application application) {
super(application);
}
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CusReactPackage()
);
}
}
生成NativeHost类,并在Application中注册
//MainApplication.java
private ReactNativeHost mReactNativeHost = new YDReactNativeHost(this);
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
至此,native部分框架就已经搭好。
3. javascript部分
//CircleImage.js
import React from 'react';
var PropTypes = require('prop-types');
import { requireNativeComponent, View } from 'react-native';
var iface = {
name: 'RCTCircleImage',
PropTypes: {
...View.propTypes // include the default view properties
}
}
var RCTCircleImage = requireNativeComponent('RCTCircleImage', iface);
class CircleImage extends React.Component {
render() {
return (
<RCTCircleImage
style={{ width: 200, height: 200 }} />
);
}
}
export default CircleImage;
requireNativeComponent用于根据名字寻找Native View,接收两个参数,第一个参数是ViewManager中getName中定义的名字,第二个iface定义属性接口。
...View.propTypes // include the default view properties
表示包含了默认React Native widget中的props,比如flexbox属性等。
4. 自定义props
4.1 native端
大多数时候默认的属性还不能满足我们在JS中使用原生控件,这个时候需要自定义props。本例子中可以设置圆形image的resource。
为了设置自定义属性,需要在ViewManager中定义属性对应的设置方法(setter),并用@ReactProps注解,@ReactProps注解接收一个name参数,表示在JS调用中的props name。
除了name,@ReactProp注解还接受以下可选的参数:defaultBoolean, defaultInt, defaultFloat。这些参数必须是对应的基础类型的值(也就是boolean, int, float),当JS端在某些情况下在组件中移除了对应的属性,这些值会被传递给setter方法,注意这个default值只对基本类型生效,对于其他的类型而言,当对应的属性删除时,null会作为默认值提供给setter方法。
这里setter方法有两个参数,第一个参数是需要设置属性的View实例,第二个是需要设置的值value,这个值参数类型目前支持的有boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap。
// ReactCircleImageManager.java
private SparseIntArray resIndexMap = new SparseIntArray();
public ReactCircleImageManager() {
resIndexMap.put(1, R.drawable.ic_share);
resIndexMap.put(2, R.drawable.splash_bottom);
resIndexMap.put(3, R.drawable.splash_img);
}
...
@ReactProp(name = "resIndex", defaultInt = 1)
public void setResIndex(CircleImageView imageView, int resIndex) {
imageView.setImageResource(resIndexMap.get(resIndex));
}
这里为了简单说明自定义props的用法,直接将Resource ID定义在native层,JS通过属性resIndex来选择需要的resource。
4.2 JS端
在JS端只需要通过propTypes来描述这些自定义的属性的类型。
//CircleImage.js
var iface = {
name: 'RCTCircleImage',
PropTypes: {
resIndex: PropTypes.number, //描述属性类型
...View.propTypes // include the default view properties
}
}
var RCTCircleImage = requireNativeComponent('RCTCircleImage', iface);
之后便可以使用这些props了。
//CircleImage.js
render() {
return (
<RCTCircleImage
resIndex={1}
style={{ width: 200, height: 200 }} />
);
}
4.3 @ReactPropGroup注解
后续补充
5. JS监听原生事件
JS端可能对native控件在运行中的一些事件感兴趣,希望能够得到原生控件的事件(event),比如组件内部状态变化的回调、触摸手势事件等。
5.1 native端
native端可以使用RCTEventEmitter将事件传递到JS端。基本的用法为
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(yourView.getId(), "topChange", event);
receiveEvent第一个参数是viewId,第二个参数是eventName,topChange对应JS接收属性为onChange,第三个参数是需要传递的event。
比如我们可以将CircleImageVIew点击事件传递到JS端,并携带一个参数,如:
//ReactCircleImageManager.java
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//第一种方式
WritableMap event = Arguments.createMap();
event.putInt("int_value", 1);
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(imageView.getId(), "topChange", event);
}
});
5.2 JS端
在JS端,我们需要将之前的iface描述对象换成一个另外一个对象,该对象使我们能够读取原始事件,并且当用户不设置onChange props时设置所需的自定义行为。
我们将requireNativeComponent方法写成如下并监听onChange事件:
CircleImage.propTypes = {
resIndex: PropTypes.number,
...View.propTypes,
}
const RCTCircleImage = requireNativeComponent('RCTCircleImage', CircleImage, {
nativeOnly: {
onChange: true,
},
});
onChange = e => {
alert(e.nativeEvent.int_value);
}
render() {
return (
<RCTCircleImage
resIndex={1}
onChange={this.onChange}
style={{ width: 200, height: 200 }} />
);
}
5.3 事件名称和JS端props对应关系
为什么事件名称topChange对应JS端onChange属性呢,好像也没有定义这个对应关系啊?其实在ViewManager中预先定义好了一些对应关系在UIManagerModuleConstants.java中:
//UIManagerModuleConstants.java
/* package */ static Map getBubblingEventTypeConstants() {
return MapBuilder.builder()
.put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange", "captured", "onChangeCapture")))
.put(
"topSelect",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onSelect", "captured", "onSelectCapture")))
...
.build();
}
那如果我们想自己定义对应关系,该怎么做呢,其实很简单,只需要复写ViewManager中getExportedCustomDirectEventTypeConstants()方法就行了。
@Nullable
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Object>builder()
.put("clickMessage", MapBuilder.of("registrationName", "onClick"))
.build();
}
这样就把clickMessage和onClick关联起来了。
5.4 关于nativeOnly
有时候有一些特殊的属性,想从原生组件中导出,但是又不希望它们成为对应React封装组件的属性。比如,一个原生onChange事件对应到JS端onChangeMessage属性,但接收参数不是raw event而是boolean。这样的话你可能不希望原生专用的属性出现在API之中,也就不希望把它放到propTypes里。可是如果你不放的话,又会出现一个报错。解决方案就是带上nativeOnly选项。
5.5 另一种方式发送事件
除了上面提到的直接使用receiveEvent方式之外,还可以使用EventDispatcher发送事件,它的好处是作为发送的中间者,用于调节真正发送事件到JS的速度,以免造成JS来不及处理的情况。首先构造一个Event的子类,包括发送的数据和EventName
package com.yuanchain.yuandian.widget.webview.event;
/**
* Event emitted when loading progress changed.
*/
public class ProgressMessageEvent extends Event<ProgressMessageEvent> {
public static final String EVENT_NAME = "progressMessage";
private final double mData;
public ProgressMessageEvent(int viewId, double data) {
super(viewId);
mData = data;
}
...
@Override
public void dispatch(RCTEventEmitter rctEventEmitter) {
WritableMap data = Arguments.createMap();
data.putDouble("data", mData);
rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data);
}
}
其次,使用EventDispatcher发送Event。
ReactContext reactContext = (ReactContext) webView.getContext();
EventDispatcher eventDispatcher =
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
eventDispatcher.dispatchEvent(event);
6. JS端直接调用View方法
直接参考webview的源码,UIManager可以把调用命令分发到Native端,Native端UIManagerModule类可以通过dispatchViewManagerCommand方法接受到JS端分发过来的调用命令,然后通过UIImplementation调用到ViewManager中进行真正的方法调用。
//UIManagerModule.java
@ReactMethod
public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArray commandArgs) {
mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs);
}
具体做法需要:
native端,在ViewManager中定义可以调用的方法命令。
@Override
public @Nullable
Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
"goBack", COMMAND_GO_BACK,
"goForward", COMMAND_GO_FORWARD,
"reload", COMMAND_RELOAD,
"stopLoading", COMMAND_STOP_LOADING;
"injectJavaScript", COMMAND_INJECT_JAVASCRIPT
);
}
@Override
public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) {
switch (commandId) {
case COMMAND_GO_BACK:
root.goBack();
break;
case COMMAND_GO_FORWARD:
root.goForward();
break;
case COMMAND_RELOAD:
root.reload();
break;
case COMMAND_STOP_LOADING:
root.stopLoading();
break;
case COMMAND_INJECT_JAVASCRIPT:
root.loadUrl("javascript:" + args.getString(0));
break;
}
}
getCommandsMap定义好JS调用的方法名称和CommandId对应关系,receiveCommand根据commandId调用相应的View方法。
JS端,调用时通过桥接调用UIManager的dispatchViewManagerCommand方法,调用到那native端的UIManagerModule的上面提到的方法。getWebViewHandle方法是找到View在视图树中的节点句柄,用于定位到相应的View。
模块数据结构,JS端可访问:
UIManager.[UI组件名].[Constants(静态值)/Commands(命令/方法)]
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goBack,
null
);
};
getWebViewHandle = () => {
return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
};
6. 参考资料
Java UI Component on React Native
React Native通讯原理
React-Native 渲染实现分析
Native UI Components