React Native 原生组件封装:将 View 作为参数传入原生组件

阅读该文章的读者,我会假设你已经有原生组件封装的经验,至少,你应该看过官方文档里关于原生组件的说明(Android 篇iOS 篇)。

让我们开始吧

View 作为参数传入原生组件,其实是一个非常有用的功能,可惜官方文档并没有做任何介绍,RN 的一些内置组件就用了这一功能,现实中的应用场景有:

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,代码很简单,总结一下:

  1. Manager 要继承 ViewGroupManager 而不是 SimpleViewManager
    public class ReactDrawerLayoutManager extends ViewGroupManager<ReactDrawerLayout>
    
  2. Override addViewremoveViewAt 实现你想要的功能。这时,第二个参数 child 就是 js 端传进来 view,已经被转化成了 ReactView
    @Override
    public void addView(ReactDrawerLayout parent, View child, int index)
    

iOS

对于 iOS 我选择的参考对象是 TabBarIOS。

从 js 端将 view 传递到 native 端的方法和 android 是一样的,不再赘述。直接关注原生代码的实现 RCTTabBar.m,总结一下:

  1. #import "UIView+React.h"
  2. 实现 insertReactSubviewremoveReactSubview
    - (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();
  }

ReactDrawerLayout.java#L63

  // 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 作为新兴技术,很多细节未被发掘,我会鼓励你多看源代码,因为在没有官方文档的情况下,代码就是文档。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容