React Native进阶之原生UI组件封装—适配Android
(一)前言
这篇来介绍一下封装原生UI组件封装给React Native前端进行调用,不过当前文章所讲只是用于Android部分开发,后面会继续更新适配iOS开发的教程。
在实际开发App开发中,其实已经有成千上万的原生UI控件通过封装之后,用在App开发中,一些事平台自带,另外一些是第三方库封装的。在React Native中也有很多组件,这些就是FaceBook给我们封装的,例如:ScrollView和TextInput组件,但是由于精力有限不可能封装所有的组件。所以,在实际开发中,通常需要自己封装组件,然后移植到我们的应用程序中。
这篇文章就来讲解一下如何方便封装原生组件,通过JS来使用,不过只能适配Android平台。下面我们进行封装实现React Native核心库的ImageView组件。
(二)ImageView组件实例解析
这节介绍一下React Native库中的ImageView封装原理以及大概步骤,在下一节,我们将都每一个步骤进行详细讲解。
首先着重介绍一下ViewManager和SimpleViewManager。SimpleViewManager是ViewManager的子类,它是比ViewManager更为简单的管理类,只不过他们的功能出发点都是差不多,都是为了创建和管理原生View。在实际开发过程中,创建一个继承自SimpleViewManager的子类,因为它可以提供更多通用简单属性,例如:Background Color(背景色)、Opacity(透明度)、FlexBox Layout(flexbox布局)。
创建的这些子类本质上都是单例的,React Native原则是只会创建一个唯一的实例,也就是单例。然后把原生View交给NativeViewHierarchyManager进行管理,并且它可以被设置为原生试图的代理,当更新属性或者调用方法进行回调操作。
具体步骤:
- 创建一个ViewManager子类
- 实现createViewInstance方法
- 使用@ReactProp(或者@ReactPropGroup)进行注解试图的属性。
- 在createViewManagers方法中注册试图管理类
- 实现JavaScript模块
官方源代码如图所示:
(三)具体步骤详解
- 创建一个ViewManager子类
在本实例中,我们创建一个ReactImageManager视图管理器,这是继承自SimpleViewMangaer < ReactImageView >类。ReactImageView是视图管理进行管理的类,这是一个自定义的原生View。当前创建的类中必须实现一个getName方法,该方法返回的名字用于该在JavaScript中进行调用。下面我们看一下具体的实例:
public class ReactImageManager extends SimpleViewManager<ReactImageView> {
public static final String REACT_CLASS="RCTImageView";
@Override
public String getName() {
return REACT_CLASS;
}
}
- 实现createViewInstance方法
视图会通过createViewInstance方法进行创建,同时视图会被初始化成默认状态。然后视图属性会通过updateView方法进行更新。具体代码如下:
@Override
protected ReactImageView createViewInstance(ThemedReactContext reactContext) {
return new ReactImageView(reactContext, Fresco.newDraweeControllerBuilder(),mCallerContext);
}
- 使用@ReactProp(或者@ReactPropGroup)进行注解视图的属性。
如果需要对外提供方法给JavaScript进行调用,那么需要使用@ReactProp(或者@ReactPropGroup)进行注解。它注解的方法的第一个参数为需要修改设置属性的具体事例,第二个参数为需要设置的属性值。被注解的方法的返回值必须为void,而且方法的访问权限必须为public。其中在JavaScript前端获取属性的类型由被注解的方法的第二个参数的类型来进行决定。支持的类型为:boolean,int,float,String,Boolean,Integer,ReadableArray,ReadableMap。
使用@ReactProp注解,注解中必须包含一个字符串类型的属性name,该参数的值指定了在JavaScript端进行调用的属性名称。
除了name属性之后,@ReactProp注解还能接收其他一些属性例如:defaultBoolean,defaultInt,defaultFloat。这些参数必须对应基础的数据类型(boolean,int,float),但是当对应的默认属性值被删除或者不设置,会使用null作为默认值。
【注意】如果使用@ReactPropGroup进行注解,那么该和@ReactProp还是有一些不同点的,具体可以查看@ReactPropGroup注解类的相关文档查看。我们来看一下官方的注解的方法代码:
// In JS this is Image.props.source.uri
@ReactProp(name = "src")
public void setSource(ReactImageView view, @Nullable String source) {
view.setSource(source, mResourceDrawableIdHelper);
}
// In JS this is Image.props.loadingIndicatorSource.uri
@ReactProp(name = "loadingIndicatorSrc")
public void setLoadingIndicatorSource(ReactImageView view, @Nullable String source) {
view.setLoadingIndicatorSource(source, mResourceDrawableIdHelper);
}
@ReactProp(name = "borderColor", customType = "Color")
public void setBorderColor(ReactImageView view, @Nullable Integer borderColor) {
if (borderColor == null) {
view.setBorderColor(Color.TRANSPARENT);
} else {
view.setBorderColor(borderColor);
}
}
@ReactProp(name = "overlayColor")
public void setOverlayColor(ReactImageView view, @Nullable Integer overlayColor) {
if (overlayColor == null) {
view.setOverlayColor(Color.TRANSPARENT);
} else {
view.setOverlayColor(overlayColor);
}
}
@ReactProp(name = "borderWidth")
public void setBorderWidth(ReactImageView view, float borderWidth) {
view.setBorderWidth(borderWidth);
}
- 进行注册ViewManager
原生代码最后一步就是进行注册ViewManager到应用中,该步骤和前面原生模块封装相类似,唯一的不同,当前是注册在createViewManagers方法,官方代码如下:
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactImageManager()
);
}
- 实现JavaScript模块
下面是最后一步是创建JavaScript模块,用来进行定义Java和JavaScript之间的接口层。大部分的功能都是通过React底层的Java和Javas进行完成,下面就是需要使用propTypes属性进行设置属性的类型。
// ImageView.js
import { PropTypes } from 'react';
import { requireNativeComponent } from 'react-native';
var iface = {
name: 'ImageView',
propTypes: {
src: PropTypes.string,
borderRadius: PropTypes.number,
resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']),
},
};
module.exports = requireNativeComponent('RCTImageView', iface);
requireNativeComponent会接收两个参数,第一个参数是代码视图的名字(也是之前我们getName方法返回的名称),第二个参数表示描述信息设置的对象。当前对象的name的值必须设置恰当,因为该值会在调试中进行打印。该对象还需要进行声明propTypes字段,用来反映原生视图。同时propTypes对象可以用来检查用户使用的原生视图的正确与否。
【注意】如果你需要JavaScript组件可以做更多的事情,而不仅仅为设置名称和propTypes,例如可以进行处理一些自定义的事件。那么你可以通过一个普通的React组件进行封装原生组件,那么requireNativeComponent的第二个参数就可以传入封装的组件而不是iface了。这些你可以在下面MyCustomView例子中看到。
(四)事件
看了上面的内容,相信大家已经对于封装原生视图并且通过前端JavaScript进行调用以及比较熟悉了,但是如果处理来自用户的事件呢?例如缩放和拖动呢?当一个原生代码的事件发生的时候,它会影响到JavaScript层的视图,两端通过getId()方法进行相关联。官方代码如下:
class MyCustomView extends View {
...
public void onReceiveNativeEvent() {
WritableMap event = Arguments.createMap();
event.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext)getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(),
"topChange",
event);
}
}
上面的事件名称”topChange”将会映射到onChange回调方法中(映射关系在UIManagerModuleConstants.java中),该回调方法会被原始事件调用,通过我们在封装的组件中做一个比较简单的API接口进行调用。
// MyCustomView.js
class MyCustomView extends React.Component {
constructor() {
this._onChange = this._onChange.bind(this);
}
_onChange(event: Event) {
if (!this.props.onChangeMessage) {
return;
}
this.props.onChangeMessage(event.nativeEvent.message);
}
render() {
return <RCTMyCustomView {...this.props} onChange={this._onChange} />;
}
}
MyCustomView.propTypes = {
/**
* Callback that is called continuously when the user is dragging the map.
*/
onChangeMessage: React.PropTypes.func,
...
};
var RCTMyCustomView = requireNativeComponent(`RCTMyCustomView`, MyCustomView, {
nativeOnly: {onChange: true}
});
注意看上面的属性nativeOnly,有时候你那边有一些被注解对外提供的特殊的属性,但是又不想把该属性变成React 组件封装的属性。例如:switch有一个onChange方法专门处理原生的事件,然后我们在进行封装该组件的时候使用onValueChange作为处理事件的回调方法。该方法被调用的时候会带上switch的相关状态信息,这样的话你可能不希望原生专用的属性出现在封装的组件的API之中,也就不希望把它放到propTypes里。可是如果你不放的话,又会出现一个报错。解决方案就是带上nativeOnly属性即可。