Oct 9

React-Native的Native UI Components整理

Lrdcq , 2016/10/09 19:07 , 教程 , 閱讀(7250) , Via 本站原創
由于新一波react-native制作的app开始开发,因此也开始继续深入的从native角度了解和使用React-Native。编写Native Modules已经是用得轻车熟路了,随着版本更新这方面的改动也不是很大并不是什么问题,而编写Native UI Components随着多端ui控件统一和业务上需要一些定制性较高针对性较高的界面元素,提上了日程。因此,在实际业务编写中便携多个Native UI Components并有一些关键的问题,记录以下。

简单界面控件

react-native官网上的文档Native UI Components(http://facebook.github.io/react-native/docs/native-components-android.html)部分就是单纯的介绍了一个简单的界面控件的编写过程。

编写提供给react-native的控件最主要的任务除了那个控件——如果需要自己封装一次,最重要的就是编写一个ViewManager了。一个简单的界面控件是继承于SimpleViewManager,其最重要的只需要实现两个关键的方法:
  @Override
  public String getName() {
    return "Name";
  }
  
  @Override
  public View createViewInstance(ThemedReactContext context) {
    return new View(context);
  }

即返回这个自定义控件的名字和实际生成的View。除此之外,最常用的功能就是为组件添加属性,通过ReactProp注解和编写set方法来实现。比如添加一个叫max的属性可以这样写:
  @ReactProp(name = "max")
  public void setMax(View view, @Nullable int max) {
    view.setWhatwhat(max);
  }

在react的世界里,操纵组件最主要的方式就是组件属性了,因此这也是实际使用中最重要的方法。

容器界面控件

很快我们就离开官方文档上的内容了。除了独立简单的界面,我们更需要的是容器性质的界面——比如漂亮的下拉刷新和界面切换器。一个容器界面控件的ViewManager是继承于ViewGroupManager,一个典型的ViewGroupManager编写起来和SimpleViewManager是一模一样的,并没有什么区别,但涉及到容器嘛,就会有子界面相关的内容一般会涉及到继承与重新实现:
  public void addView(ViewGroup parent, View child, int index) {
    parent.addView(child, index);
    reorderChildrenByZIndex(parent);
  }

在界面初始化的时候,会通过addView往ViewGroup中添加子View,如果需要对子view做限制或者处理,在这里当然最合适了。同时当然还有对称的removeView可以用来处理。另外还有两个开关:
  public boolean needsCustomLayoutForChildren() {
    return false;
  }
  public boolean shouldPromoteGrandchildren() {
    return false;
  }

needsCustomLayoutForChildren是问的,这个ViewGroup的子控件是否是自定义布局。如果是,那么子控件就全部依赖ViewGroup的native代码了,否则,react会认jsx中编写的css样式去处理。一般自定义轮播器等显然以布局为核心的自定义控件就不能让css去插手了。
shouldPromoteGrandchildren主要针对子控件只有一个view的控件,确定其处理孙子控件的方式。对于这种情况,reactnative的native部分针对这种情况做了一些优化,这就是这个开关。

CSS样式布局

要自定义控件自身的css样式布局处理方式,需要重写这两个方法并且重新实现一份LayoutShadowNode:
  @Override
  public LayoutShadowNode createShadowNodeInstance() {
    return new LayoutShadowNode();
  }

  @Override
  public Class<? extends LayoutShadowNode> getShadowNodeClass() {
    return LayoutShadowNode.class;
  }

其实LayoutShadowNode是ViewManager基类里最重要的两个范型之一,用于处理不限于布局的控件的style也就是css节点样式。比如,我们看到TextInput控件中实现的ReactTextInputShadowNode,它主要做的事情包括:
1.添加更多的css属性,例如:
  @ReactProp(name = "mostRecentEventCount")
  public void setMostRecentEventCount(int mostRecentEventCount) {
    mJsEventCount = mostRecentEventCount;
  }

2.利用现有信息进行重新布局:
  @Override
  public void measure(
      CSSNodeAPI node,
      float width,
      CSSMeasureMode widthMode,
      float height,
      CSSMeasureMode heightMode,
      MeasureOutput measureOutput) {
    // measure() should never be called before setThemedContext()
    EditText editText = Assertions.assertNotNull(mEditText);

  ...
  
    editText.measure(
        MeasureUtil.getMeasureSpec(width, widthMode),
        MeasureUtil.getMeasureSpec(height, heightMode));
    measureOutput.width = editText.getMeasuredWidth();
    measureOutput.height = editText.getMeasuredHeight();
  }

这个测量的方法实质上是实现的CSSNodeAPI.MeasureFunction,具体的写法其实是新建了一个空白native控件,塞入数据和属性进行测量再取出。当然,还做了很多其他有的没的的操作来完成这整个过程。

3.最后布局样式操作完成之后,会调用
 public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) {
   Spannable preparedSpannableText = fromTextCSSNode(this);
      ReactTextUpdate reactTextUpdate =
        new ReactTextUpdate(
          preparedSpannableText,
          mJsEventCount,
          mContainsImages,
          getPadding(),
          getEffectiveLineHeight(),
          mTextAlign
        );
      uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate); // ~!!!!!!
  }

这个方法是在css样式的更新同步到node类型完成之后调用,通过这个方法把最终需要更新到view的信息打包为一个object对象,并通过uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);把这个对象发送出来。最后回到ViewManager,重写updateExtraData把传送过来的数据解开并实际应用到界面上,形如:
  @Override
  public void updateExtraData(ReactEditText view, Object extraData) {
    if (extraData instanceof float[]) {
      float[] padding = (float[]) extraData;

      view.setPadding(
          (int) Math.ceil(padding[0]),
          (int) Math.ceil(padding[1]),
          (int) Math.ceil(padding[2]),
          (int) Math.ceil(padding[3]));
    } else if (extraData instanceof ReactTextUpdate) {
      ReactTextUpdate update = (ReactTextUpdate) extraData;
      if (update.containsImages()) {
        Spannable spannable = update.getText();
        TextInlineImageSpan.possiblyUpdateInlineImageSpans(spannable, view);
      }
      view.maybeSetText(update);
    }
  }

总之,如果需要添加新的style样式,css这种东西,就需要做如上行为,只是稍显繁琐。

事件

对于每个组件,除了各种属性和css样式,接下来最重要的当然是接收各种事件了。官方文档上写了一种传送事件的方式,但是那是简化且曲线救国的方式,我们参考内部代码,在复杂的控件上还是使用标准的事件传送方式。
在native控件中每一个事件都是一个Event对象实例,比如下拉刷新控件的相应事件:
public class RefreshEvent extends Event<RefreshEvent> {

    protected RefreshEvent(int viewTag) {
        super(viewTag);
    }

    @Override
    public String getEventName() {
        return "topRefresh";
    }

    @Override
    public void dispatch(RCTEventEmitter rctEventEmitter) {
        rctEventEmitter.receiveEvent(getViewTag(), getEventName(), null);
    }
}

每个事件需要有它的储存内容和发送方法dispatch,其中rctEventEmitter.receiveEvent的第三个参数是事件的事件体内容是WritableArray,在上面传递了空。
根据事件情况定义完成大量事件后,我们只需要回到ViewManager,重写方法:
  @Override
  protected void addEventEmitters(
      final ThemedReactContext reactContext,
      final ReactSwipeRefreshLayout view) {
    view.setOnRefreshListener(
        new OnRefreshListener() {
          @Override
          public void onRefresh() {
            reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher()
                .dispatchEvent(new RefreshEvent(view.getId()));
          }
        });
  }
  
  @Override
  public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
    return MapBuilder.<String, Object>builder()
        .put("topRefresh", MapBuilder.of("registrationName", "onRefresh"))
        .build();
  }
或者通过其他方式getEventDispatcher获取到事件发神器并发送事件,即可。同时在最后,通过重写getExportedCustomDirectEventTypeConstants把每一个事件注册到ViewManager中,只有这样控件才会有对应的事件属性。

常量和方法

当然,每一个控件既然是一个类或者对象,当然也能提供常量和方法来拓展和丰富它的功能。这些东西在简单的react代码中用得很少,在特殊的控件(比如alert)中却非常重要。注册常量简直不能太简单,直接重写getExportedViewConstants方法即可,例如:
  @Override
  public @Nullable Map getExportedViewConstants() {
    return MapBuilder.of(
        "AutoCapitalizationType",
        MapBuilder.of(
            "none",
            0,
            "characters",
            InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS,
            "words",
            InputType.TYPE_TEXT_FLAG_CAP_WORDS,
            "sentences",
            InputType.TYPE_TEXT_FLAG_CAP_SENTENCES));
  }

除了常量,我们还希望提供方法,然而ViewManager让我们提供的是一个类似于命令的东西。首先通过重写getCommandsMap方法注册命令,同时通过receiveCommand方法接收触发的命令并且进行操作就可以了。下面是WebView控件提供命令的方式:
  @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);
  }

  @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;
    }
  }

同时,由于并不是普通的方法,因此js部分也有特殊的处理方式,是:
  goForward = () => {
    UIManager.dispatchViewManagerCommand(
      this.getWebViewHandle(),
      UIManager.RCTWebView.Commands.goForward,
      null
    );
  };

于此,基本覆盖Native UI Components的全部基本内容,js部分按照reactjs习惯再封装一次就可以看到我们平时所用控件了。
logo