React Native已经封装了大部分最常见的组件,譬如ScrollView和TextInput,但不可能封装全部组件。而且,说不定你曾经为自己以前的App还封装过一些组件,React Native肯定没法包含它们。幸运的是,在React Naitve应用程序中封装和植入已有的组件非常简单。
原生视图需要被一个ViewManager的派生类(或者更常见的,SimpleViewManager的派生类)创建和管理。一个SimpleViewManager可以用于这个场景,是因为它能够包含更多公共的属性,譬如背景颜色、透明度、Flexbox布局等等。
这些子类本质上都是单例——React Native只会为每个管理器创建一个实例。它们创建原生的视图并提供给NativeViewHierarchyManager,NativeViewHierarchyManager则会反过来委托它们在需要的时候去设置和更新视图的属性。ViewManager还会代理视图的所有委托,并给JavaScript发回对应的事件。
提供原生视图很简单:
创建一个ViewManager的子类。
实现createViewInstance方法。
导出视图的属性设置器:使用@ReactProp(或@ReactPropGroup)注解。
把这个视图管理类注册到应用程序包的createViewManagers里。
实现JavaScript模块。
这里,我们以WebView举例 来实现封装原生UI组件的功能.
1. 创建ViewManager的子类
在这个例子里我们创建一个视图管理类ReactWebViewManager,它继承自SimpleViewManager。ReactImageView是这个视图管理类所管理的对象类型,这应当是一个自定义的原生视图。getName方法返回的名字会用于在JavaScript端引用这个原生视图类型。
...
public class ReactWebViewManager extends SimpleViewManager<ReactImageView> {
@Override
public String getName() {
return "AndroidRCTWebView";
}
2. 实现方法createViewInstance
视图在createViewInstance中创建,且应当把自己初始化为默认的状态。所有属性的设置都通过后续的updateView来进行。
@Override
protected WebView createViewInstance(ThemedReactContext reactContext) {
WebView webView = new WebView(reactContext);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
return webView;
}
3. 通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。
要导出给JavaScript使用的属性,需要申明带有@ReactProp(或@ReactPropGroup)注解的设置方法。方法的第一个参数是要修改属性的视图实例,第二个参数是要设置的属性值。方法的返回值类型必须为void,而且访问控制必须被声明为public。JavaScript所得知的属性类型会由该方法第二个参数的类型来自动决定。支持的类型有:boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap。
@ReactProp注解必须包含一个字符串类型的参数name。这个参数指定了对应属性在JavaScript端的名字。
除了name,@ReactProp注解还接受这些可选的参数:defaultBoolean, defaultInt, defaultFloat。这些参数必须是对应的基础类型的值(也就是boolean, int, float),这些值会被传递给setter方法,以免JavaScript端某些情况下在组件中移除了对应的属性。注意这个”默认”值只对基本类型生效,对于其他的类型而言,当对应的属性删除时,null会作为默认值提供给方法。
使用@ReactPropGroup来注解的设置方法和@ReactProp不同。请参见@ReactPropGroup注解类源代码中的文档来获取更多详情。
重要! 在ReactJS里,修改一个属性会引发一次对设置方法的调用。有一种修改情况是,移除掉之前设置的属性。在这种情况下设置方法也一样会被调用,并且“默认”值会被作为参数提供(对于基础类型来说可以通过defaultBoolean、defaultFloat等@ReactProp的属性提供,而对于复杂类型来说参数则会设置为null)
@ReactProp(name = "url")
public void setUrl(WebView view, @Nullable String url) {
Log.e("TAG", "setUrl");
view.loadUrl(url);
}
@ReactProp(name = "html")
public void setHtml(WebView view, @Nullable String html) {
Log.e("TAG", "setHtml");
view.loadData(html, "text/html; charset=utf-8", "UTF-8");
}
至此 , 我们的ReactWebViewManager类的内容就算写完了,下面是源码:
package com.githubdemo.reactManager;
import android.support.annotation.Nullable;
import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
/**
* Created by dongfanggouwu-zy on 2018/4/2.
*/
public class ReactWebViewManager extends SimpleViewManager<WebView> {
@Override
public String getName() {
return "AndroidRCTWebView";
}
@Override
protected WebView createViewInstance(ThemedReactContext reactContext) {
WebView webView = new WebView(reactContext);
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
return webView;
}
@ReactProp(name = "url")
public void setUrl(WebView view, @Nullable String url) {
Log.e("TAG", "setUrl");
view.loadUrl(url);
}
@ReactProp(name = "html")
public void setHtml(WebView view, @Nullable String html) {
Log.e("TAG", "setHtml");
view.loadData(html, "text/html; charset=utf-8", "UTF-8");
}
}
4. 注册ViewManager
在Java中的最后一步就是把视图控制器注册到应用中。这和原生模块的注册方法类似,唯一的区别是我们把它放到createViewManagers方法的返回值里。
package com.githubdemo.reactManager;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Created by dongfanggouwu-zy on 2018/4/2.
*/
public class AppReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactWebViewManager()
);
}
}
当然不要忘了 这个package需要在MainApplication.java文件的getPackages方法中提供:
public class MainApplication extends Application implements ReactApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new AppReactPackage() //这个是我们自己要加的
);
}
@Override
protected String getJSMainModuleName() {
return "index";
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, /* native exopackage */ false);
}
}
这里有一个需要注意的地方ReactWebViewManager 是我们自己写的,所以导入包的时候 不要导错了 ,如果你自己起了别的名字 可能会跟android内部的一些类名方法相同,需要特别注意.
5. 实现对应的JavaScript模块
整个过程的最后一步就是创建JavaScript模块并且定义Java和JavaScript之间的接口层。大部分过程都由React底层的Java和JavaScript代码来完成,你所需要做的就是通过propTypes来描述属性的类型。
// WebView.js
/**
* Created by 卓原 on 2018/4/2.
* zhuoyuan93@gmail.com
*/
import {
requireNativeComponent,
View,
} from 'react-native';
import PropTypes from 'prop-types';
let iface = {
name: 'WebView',
propTypes: {
url: PropTypes.string,
html: PropTypes.string,
...View.propTypes // 包含默认的View的属性
}
};
module.exports = requireNativeComponent('AndroidRCTWebView', iface);
requireNativeComponent通常接受两个参数,第一个参数是原生视图的名字,而第二个参数是一个描述组件接口的对象。组件接口应当声明一个友好的name,用来在调试信息中显示;组件接口还必须声明propTypes字段,用来对应到原生视图上。这个propTypes还可以用来检查用户使用View的方式是否正确。
注意,如果你还需要一个JavaScript组件来做一些除了指定name和propTypes以外的事情,譬如事件处理,你可以把原生组件用一个普通React组件封装。在这种情况下,requireNativeComponent的第二个参数变为用于封装的组件。这个在后文的例子里面用到。
译注:和原生模块不同,原生视图的前缀RCT不会被自动去掉。
正式使用:
/**
* Created by 卓原 on 2018/3/1.
* zhuoyuan93@gmail.com
*/
import React from 'react';
import {
View
} from 'react-native';
import WebView from '../common/WebView';
export default class FavoritePage extends React.Component {
render() {
return (
<View>
<WebView
url="https://www.baidu.com"
style={{width: 200, height: 400}}/>
</View>
)
}
}
现在 你封装的原生组件就已经可以在RN上面使用了.
但是,这还只是最基础的将原始UI组件显示出来,如果你想再RN端进行一些事件处理的话,还要一些操作,而更为常见的却是事件,比如我们需要在javascript层处理这个WebView的滚动事件,这时候又要怎么做呢。
事件
现在我们已经知道了怎么导出一个原生视图组件,并且我们可以在JS里很方便的控制它了。不过我们怎么才能处理来自用户的事件,譬如缩放操作或者拖动?当一个原生事件发生的时候,它应该也能触发JavaScript端视图上的事件,这两个视图会依据getId()而关联在一起。
这时候我们就需要继承WebView,重写对应的事件,然后将事件传递给javascript层了
package com.githubdemo.rctViews;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.webkit.WebView;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
/**
* Created by dongfanggouwu-zy on 2018/4/2.
*/
public class RCTWebView extends WebView {
public RCTWebView(Context context) {
super(context);
}
public RCTWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RCTWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
Log.e("TAG", "onScrollChanged");
WritableMap event = Arguments.createMap();
event.putInt("ScrollX", l);
event.putInt("ScrollY", t);
ReactContext reactContext = (ReactContext) getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(), "topChange", event);
}
}
我们重写了滚动时回调的onScrollChanged方法,构造了一个WritableMap 对象,将ScrollX和ScrollY传入,然后调用reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(getId(), “topChange”, event);将事件发生到javascript层,注意topChange对应着javascript层的onChange方法,这个映射关系在UIManagerModuleConstants类中。
然后我们需要修改ReactWebViewManager 中的createViewInstance方法,在里面返回我们实现的子类,就像这样子
...
public class ReactWebViewManager extends SimpleViewManager<WebView> {
...
@Override
protected WebView createViewInstance(ThemedReactContext reactContext) {
WebView webView = new RCTWebView(reactContext); //这里就改成我们重写的WebView
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
});
return webView;
}
...
}
而javascript层也需要进行一定程度的改造,以前的WebView.js已经无法满足我们的需求了.最终的代码如下,
/**
* Created by 卓原 on 2018/4/2.
* zhuoyuan93@gmail.com
*/
'use strict';
import React from 'react';
import {
requireNativeComponent,
View
} from 'react-native';
import PropTypes from 'prop-types';
var RCTWebView = requireNativeComponent('AndroidRCTWebView', WebView, {
nativeOnly: {onChange: true}
});
export default class WebView extends React.Component {
static propTypes = {
url: PropTypes.string,
html: PropTypes.string,
onScrollChange: PropTypes.func,
...View.propTypes // 包含默认的View的属性
};
constructor(props) {
super(props);
}
_onChange(event: Event) {
if (!this.props.onScrollChange) {
return;
}
this.props.onScrollChange({ScrollX: event.nativeEvent.ScrollX, ScrollY: event.nativeEvent.ScrollY});
}
render() {
return (
<RCTWebView {...this.props} onChange={(event) => this._onChange(event)}/>
)
}
}
/*
let iface = {
name: 'WebView',
propTypes: {
url: PropTypes.string,
html: PropTypes.string,
...View.propTypes // 包含默认的View的属性
}
};
module.exports = requireNativeComponent('AndroidRCTWebView', iface);*/
在onChange函数中,我们进行判断,如果属性onScrollChange没有设置或者为false,就直接return,否则就调用设置的onScrollChange属性值(该值是一个函数类型),将Java层传入的两个参数传到该函数中去,{ScrollX:event.nativeEvent.ScrollX,ScrollY:event.nativeEvent.ScrollY}
然后我们来进行调用:
export default class FavoritePage extends React.Component {
constructor(props) {
super(props);
this.state = {
a: 22
}
}
render() {
return (
<View>
<WebView
onScrollChange={this.onWebViewScroll}
url={"https://www.baidu.com"}
style={{width: 200, height: 200}}/>
<Text onPress={() => this.setState(preState => {
a: preState.a++
}, console.log(this.state.a))}>aaa</Text>
</View>
)
}
onWebViewScroll(event) {
console.log(event);
}
}
之后我们在WebView上滑动 就会在后台打印数来数据了~