阅读该文章的读者,我会假设你已经有原生组件封装的经验,至少,你应该看过官方文档里关于原生组件的说明(Android 篇,iOS 篇)。
让我们开始吧
View 作为参数传入原生组件,其实是一个非常有用的功能,可惜官方文档并没有做任何介绍,RN 的一些内置组件就用了这一功能,现实中的应用场景有:
- 用于 Layout 类的原生组件,比如 DrawerLayoutAndroid、ScrollView
- 实现原生组件嵌套,适合有从属关系的原生组件,比如 TabBarIOS 和 TabBarIOS.Item、MapView 和 Marker
Android
既然没有官方文档,那么只能参考内部组件的实现,DrawerLayoutAndroid 就是一个不错的参考对象。
首先,我们来看看在 js 端,view 是怎么传给原生组件的。
客户端调用:
var navigationView = (
<View style={{flex: 1, backgroundColor: '#fff'}}>
<Text style={{margin: 10, fontSize: 15, textAlign: 'left'}}>I'm in the Drawer!</Text>
</View>
);
<DrawerLayoutAndroid
drawerWidth={300}
drawerPosition={DrawerLayoutAndroid.positions.Left}
renderNavigationView={() => navigationView}>
<View style={{flex: 1, alignItems: 'center'}}>
<Text style={{margin: 10, fontSize: 15, textAlign: 'right'}}>Hello</Text>
<Text style={{margin: 10, fontSize: 15, textAlign: 'right'}}>World!</Text>
</View>
</DrawerLayoutAndroid>
内部实现 DrawerLayoutAndroid.android.js:
render: function() {
var drawStatusBar = Platform.Version >= 21 && this.props.statusBarBackgroundColor;
var drawerViewWrapper =
<View
style={[
styles.drawerSubview,
{width: this.props.drawerWidth, backgroundColor: this.props.drawerBackgroundColor}
]}
collapsable={false}>
{this.props.renderNavigationView()}
{drawStatusBar && <View style={styles.drawerStatusBar} />}
</View>;
var childrenWrapper =
<View ref={INNERVIEW_REF} style={styles.mainSubview} collapsable={false}>
{drawStatusBar &&
<StatusBar
translucent
backgroundColor={this.state.statusBarBackgroundColor}
/>}
{drawStatusBar &&
<View style={[
styles.statusBar,
{backgroundColor: this.props.statusBarBackgroundColor}
]} />}
{this.props.children}
</View>;
return (
<AndroidDrawerLayout
{...this.props}
ref={RK_DRAWER_REF}
drawerWidth={this.props.drawerWidth}
drawerPosition={this.props.drawerPosition}
drawerLockMode={this.props.drawerLockMode}
style={[styles.base, this.props.style]}
onDrawerSlide={this._onDrawerSlide}
onDrawerOpen={this._onDrawerOpen}
onDrawerClose={this._onDrawerClose}
onDrawerStateChanged={this._onDrawerStateChanged}>
{childrenWrapper}
{drawerViewWrapper}
</AndroidDrawerLayout>
);
},
我们可以看到,不管是 DrawerLayoutAndroid 的 NavigationView,还是 ContentView,都是通过 AndroidDrawerLayout 的 children 传递给原生的。这有告诉我们一个信息,一般的 props 无法传递 view,只能通过 this.children。
接下来,native 端是怎么处理传进来的 view 的呢?找到相应的原生代码:ReactDrawerLayoutManager.java,代码很简单,总结一下:
- Manager 要继承 ViewGroupManager 而不是 SimpleViewManager
public class ReactDrawerLayoutManager extends ViewGroupManager<ReactDrawerLayout>
- Override
addView
和removeViewAt
实现你想要的功能。这时,第二个参数child
就是 js 端传进来 view,已经被转化成了 ReactView@Override public void addView(ReactDrawerLayout parent, View child, int index)
iOS
对于 iOS 我选择的参考对象是 TabBarIOS。
从 js 端将 view 传递到 native 端的方法和 android 是一样的,不再赘述。直接关注原生代码的实现 RCTTabBar.m,总结一下:
#import "UIView+React.h"
- 实现
insertReactSubview
和removeReactSubview
- (void)insertReactSubview:(id <RCTComponent>)subview atIndex:(NSInteger)atIndex - (void)removeReactSubview:(id <RCTComponent>)subview
稍微复杂点的情况
看下来,你会发现要实现 view 的传递还挺简单的。
现在,让我们稍微作延伸,不知道你有没有注意到,DrawerLayoutAndroid 是靠 children 的顺序区分 NavigationView 和 ContentView 的。
ReactDrawerLayoutManager.java#L164:
/**
* This method is overridden because of two reasons:
* 1. A drawer must have exactly two children
* 2. The second child that is added, is the navigationView, which gets panned from the side.
*/
@Override
public void addView(ReactDrawerLayout parent, View child, int index) {
if (getChildCount(parent) >= 2) {
throw new
JSApplicationIllegalArgumentException("The Drawer cannot have more than two children");
}
if (index != 0 && index != 1) {
throw new JSApplicationIllegalArgumentException(
"The only valid indices for drawer's child are 0 or 1. Got " + index + " instead.");
}
parent.addView(child, index);
parent.setDrawerProperties();
}
// Sets the properties of the drawer, after the navigationView has been set.
/* package */ void setDrawerProperties() {
if (this.getChildCount() == 2) {
View drawerView = this.getChildAt(1);
LayoutParams layoutParams = (LayoutParams) drawerView.getLayoutParams();
layoutParams.gravity = mDrawerPosition;
layoutParams.width = mDrawerWidth;
drawerView.setLayoutParams(layoutParams);
drawerView.setClickable(true);
}
}
这种处理方法在 children 不多,且固定的情况下还好,但如果要传递的 View 的类型和数量比较多,这种方法就并不适用了。
一种不错的做法是通过类型区分:
@Override
public void addView(T parent, View child, int index) {
if (parent instanceof OtherType) {
...
}
}
- (void)insertReactSubview:(id <RCTComponent>)subview atIndex:(NSInteger)atIndex {
if ([subview isKindOfClass:[OtherType class]]) {
...
}
}
当然这种方法实现起来会比较麻烦,因为每一种 SubView 都要实现相应的原生组件。现实中的应用,可以参考 react-native-maps 或由我维护的 RN 高德地图组件:react-native-amap3d。
关于 ViewProps#collapsable
你可能不会注意到,View 有一个 android only 的 collapsable 属性,一般是没什么用啦,但做 Android 原生组件封装却不能不知。
Views that are only used to layout their children or otherwise don't draw anything may be automatically removed from the native hierarchy as an optimization. Set this property to false to disable this optimization and ensure that this View exists in the native view hierarchy.
意思是仅用于布局或不显示任何东西的 view 会被自动移除,这点还挺重要的,不然你可能会遇到明明在 js 端传递了 view,native 端却没有收到的情况 。DrawerLayoutAndroid 就有用这个属性。
最后
这篇文章只是一个引子,RN 作为新兴技术,很多细节未被发掘,我会鼓励你多看源代码,因为在没有官方文档的情况下,代码就是文档。