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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容