美文网首页RN And FlutterReactNative
React Native自定义原生控件(Android)

React Native自定义原生控件(Android)

作者: vince_9861 | 来源:发表于2018-10-07 19:02 被阅读305次

    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

    相关文章

      网友评论

        本文标题:React Native自定义原生控件(Android)

        本文链接:https://www.haomeiwen.com/subject/bxganftx.html