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

推荐阅读更多精彩内容