React Native 已经封装了大部分常见组件,如 ScrollView 和 TextInput,但不可能涵盖所有组件。有时开发者可能需要封装自己的原生组件,幸运的是在 React Native 中封装和植入已有组件非常简单。
原生视图需要通过 ViewManager 的派生类(通常继承 SimpleViewManager)来创建和管理。SimpleViewManager 适用于常见场景,能处理背景颜色、透明度、Flexbox 布局等公共属性。这些管理器本质上是单例,React Native 只会为每个管理器创建一个实例。它们创建原生视图并交给 NativeViewHierarchyManager,后者在需要时委托它们设置和更新视图属性。ViewManager 还代理视图的所有委托,并向 JavaScript 发送对应事件。
封装原生视图的基本步骤如下:
- 创建 ViewManager 的子类。
- 实现 createViewInstance 方法。
- 使用 @ReactProp 或 @ReactPropGroup 注解导出属性设置器。
- 将视图管理类注册到应用程序包的 createViewManagers 中。
- 实现对应的 JavaScript 模块。
本文以 WebView 为例,演示如何封装原生 Android UI 组件。
1. 创建 ViewManager 的子类
创建 ReactWebViewManager 类,继承自 SimpleViewManager。getName 方法返回的名称用于在 JavaScript 端引用该原生视图类型。
public class ReactWebViewManager extends SimpleViewManager<WebView> {
@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 注解导出属性设置方法
使用 @ReactProp 注解导出属性设置方法,供 JavaScript 使用。方法第一个参数是视图实例,第二个参数是属性值,返回类型为 void。支持的类型包括 boolean、int、float、double、String、Boolean、Integer、ReadableArray、ReadableMap。
@ReactProp(name = "url")
public void setUrl(WebView view, @Nullable String url) {
view.loadUrl(url);
}
@ReactProp(name = "html")
public void setHtml(WebView view, @Nullable String html) {
view.loadData(html, "text/html; charset=utf-8", "UTF-8");
}
完整的 ReactWebViewManager 类源码如下:
import android.support.annotation.Nullable;
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;
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) {
view.loadUrl(url);
}
@ReactProp(name = "html")
public void setHtml(WebView view, @Nullable String html) {
view.loadData(html, "text/html; charset=utf-8", "UTF-8");
}
}
4. 注册 ViewManager
在 Java 中将视图控制器注册到应用中,通过实现 ReactPackage 接口,并在 createViewManagers 方法中返回视图管理类列表。
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;
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()
);
}
}
然后在 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, false);
}
}
注意导入包时避免与 Android 内部类名冲突。
5. 实现对应的 JavaScript 模块
创建 JavaScript 模块,通过 propTypes 描述属性类型,并使用 requireNativeComponent 引用原生组件。
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 的第一个参数是原生视图名称,第二个参数是描述组件接口的对象。propTypes 用于检查属性使用是否正确。如果需要在 JavaScript 端处理事件,可以将原生组件封装在普通 React 组件中。
使用封装的原生组件
在 React Native 组件中导入并使用 WebView:
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>
);
}
}
现在,封装的原生组件可以在 React Native 中使用。但为了处理事件(如滚动),需要进一步操作。
事件处理
为了在 JavaScript 层处理原生事件(如 WebView 的滚动),需要继承 WebView 并重写事件方法,将事件传递给 JavaScript 层。
创建 RCTWebView 类,重写 onScrollChanged 方法:
import android.content.Context;
import android.util.AttributeSet;
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;
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);
WritableMap event = Arguments.createMap();
event.putInt("ScrollX", l);
event.putInt("ScrollY", t);
ReactContext reactContext = (ReactContext) getContext();
reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
getId(), "topChange", event);
}
}
然后修改 ReactWebViewManager 的 createViewInstance 方法,返回 RCTWebView 实例:
@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 组件以处理事件:
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
};
_onChange(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)}/>
);
}
}
onChange 函数映射到 Java 层的 topChange 事件,并通过 onScrollChange 属性传递滚动数据。
最后,在组件中使用并处理滚动事件:
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}}/>
</View>
);
}
onWebViewScroll(event) {
console.log(event);
}
}
现在,当在 WebView 上滑动时,滚动事件数据会在控制台打印出来。通过这种方式,可以扩展封装的原生组件以支持复杂交互。