Oct
9
由于新一波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,其最重要的只需要实现两个关键的方法:
即返回这个自定义控件的名字和实际生成的View。除此之外,最常用的功能就是为组件添加属性,通过ReactProp注解和编写set方法来实现。比如添加一个叫max的属性可以这样写:
在react的世界里,操纵组件最主要的方式就是组件属性了,因此这也是实际使用中最重要的方法。
容器界面控件
很快我们就离开官方文档上的内容了。除了独立简单的界面,我们更需要的是容器性质的界面——比如漂亮的下拉刷新和界面切换器。一个容器界面控件的ViewManager是继承于ViewGroupManager,一个典型的ViewGroupManager编写起来和SimpleViewManager是一模一样的,并没有什么区别,但涉及到容器嘛,就会有子界面相关的内容一般会涉及到继承与重新实现:
在界面初始化的时候,会通过addView往ViewGroup中添加子View,如果需要对子view做限制或者处理,在这里当然最合适了。同时当然还有对称的removeView可以用来处理。另外还有两个开关:
needsCustomLayoutForChildren是问的,这个ViewGroup的子控件是否是自定义布局。如果是,那么子控件就全部依赖ViewGroup的native代码了,否则,react会认jsx中编写的css样式去处理。一般自定义轮播器等显然以布局为核心的自定义控件就不能让css去插手了。
shouldPromoteGrandchildren主要针对子控件只有一个view的控件,确定其处理孙子控件的方式。对于这种情况,reactnative的native部分针对这种情况做了一些优化,这就是这个开关。
CSS样式布局
要自定义控件自身的css样式布局处理方式,需要重写这两个方法并且重新实现一份LayoutShadowNode:
其实LayoutShadowNode是ViewManager基类里最重要的两个范型之一,用于处理不限于布局的控件的style也就是css节点样式。比如,我们看到TextInput控件中实现的ReactTextInputShadowNode,它主要做的事情包括:
1.添加更多的css属性,例如:
2.利用现有信息进行重新布局:
这个测量的方法实质上是实现的CSSNodeAPI.MeasureFunction,具体的写法其实是新建了一个空白native控件,塞入数据和属性进行测量再取出。当然,还做了很多其他有的没的的操作来完成这整个过程。
3.最后布局样式操作完成之后,会调用
这个方法是在css样式的更新同步到node类型完成之后调用,通过这个方法把最终需要更新到view的信息打包为一个object对象,并通过uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), reactTextUpdate);把这个对象发送出来。最后回到ViewManager,重写updateExtraData把传送过来的数据解开并实际应用到界面上,形如:
总之,如果需要添加新的style样式,css这种东西,就需要做如上行为,只是稍显繁琐。
事件
对于每个组件,除了各种属性和css样式,接下来最重要的当然是接收各种事件了。官方文档上写了一种传送事件的方式,但是那是简化且曲线救国的方式,我们参考内部代码,在复杂的控件上还是使用标准的事件传送方式。
在native控件中每一个事件都是一个Event对象实例,比如下拉刷新控件的相应事件:
每个事件需要有它的储存内容和发送方法dispatch,其中rctEventEmitter.receiveEvent的第三个参数是事件的事件体内容是WritableArray,在上面传递了空。
根据事件情况定义完成大量事件后,我们只需要回到ViewManager,重写方法:
常量和方法
当然,每一个控件既然是一个类或者对象,当然也能提供常量和方法来拓展和丰富它的功能。这些东西在简单的react代码中用得很少,在特殊的控件(比如alert)中却非常重要。注册常量简直不能太简单,直接重写getExportedViewConstants方法即可,例如:
除了常量,我们还希望提供方法,然而ViewManager让我们提供的是一个类似于命令的东西。首先通过重写getCommandsMap方法注册命令,同时通过receiveCommand方法接收触发的命令并且进行操作就可以了。下面是WebView控件提供命令的方式:
同时,由于并不是普通的方法,因此js部分也有特殊的处理方式,是:
于此,基本覆盖Native UI Components的全部基本内容,js部分按照reactjs习惯再封装一次就可以看到我们平时所用控件了。
简单界面控件
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习惯再封装一次就可以看到我们平时所用控件了。