ReactNative: 构建原生的Native UI组件(此文以系统内置视图为例,自定义视图的构建原理相同,自定义一个view导出即可)

一、前言

2020,一个不平凡的悲痛的庚子年,新年伊始,湖北武汉就发生了令人始料不及的疫情。一夜间,全国都停止了脚步,隔离在家,共同抗疫。中华民族的凝聚力历来强大,几个月的努力,上万人的付出,如今春暖花开,疫情基本快要控制住了。这场战役中,牺牲了和去世了太多人,令人痛惜,为活着的人祝福,为逝去的人祷告。哪来的岁月静好,只不过是有人替我们负重前行。学习依旧,回归博客。

 

二、简介

在ReactNative开发中,ReactNative提供了很多已经封装好的基础组件,在前面的文章中有很多实践。虽然这些基础的组件可以通过组合成复合组件来实现复杂的功能,但是在性能上稍有不足。原生组件经过长时间的积累和更新,很多优秀的原生UI组件可以极大地提升性能和开发效率,ReactNative可以将它们抽象成ReactJS的组件对象提供给JavaScript端使用,也即构建原生的Native UI组件。Native UI组件实质是就是一个Native模块,跟构建的Native API组件类似,它还需要被抽象出提供给React使用的标签,如标签属性、响应用户行为等。在React中创建UI组件时,都会生成reactTag来作为唯一标识。JavaScript UI与Native UI都将通过reactTag进行关联。JavaScript UI的更新会通过调用RCTUIManager模块的方法来映射成Native UI的更新。当Native UI被通知改变时,会通过reactTag来定位UI实例来进行更新操作,所有的UI更新并不会马上执行,而是被缓存到一个UIBlocks中,每次通信完毕后,再由主线程统一执行UIBlocks中的更新。在帧级别的通信频率下,让Native UI无缝响应JavaScript的改变。

 

三、构建

1、UI组件定义

要构建Native UI组件,必须要先创建Native UI组件的管理类,这个管理类继承自RCTViewManager类,遵循RCTBridgeModule协议,导出模块类,重写-(UIView *)view接口返回Native UI实例。注意,Native UI组件的样式完全是由JavaScript来控制的,所以在这个接口内部设置UI的任何样式都会被JavaScript的样式覆盖。一般不需要对返回的Native UI实例设置frame,如果该组件内部的UI或者图层不支持自适应,则需要在UI组件的-(void)layoutSubviews方法中自适应布局。构建如下:

OC:

//  ReactNativeCustomUIDemo
//  Created by 夏远全 on 2020/3/9.
//  Copyright © 2020 Facebook. All rights reserved.

#import <React/RCTViewManager.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTMapViewManager : RCTViewManager

@end

NS_ASSUME_NONNULL_END
//  ReactNativeCustomUIDemo
//  Created by 夏远全 on 2020/3/9.
//  Copyright © 2020 Facebook. All rights reserved.

#import "RCTMapViewManager.h"
#import <MapKit/MapKit.h>

@implementation RCTMapViewManager

//导出模块类
RCT_EXPORT_MODULE();

//返回Native UI
-(UIView *)view
{
  //地图
  MKMapView *mapView = [[MKMapView alloc] init];

  //样式
  mapView.mapType = MKMapTypeStandard;
  
  return mapView;
}

@end

2、UI组件使用

参照系统的命名规范,扩展的Native UI组件模块都是以Manager为后缀,在使用时只需要在JavaScript中导出对应的原生组件对象即可。组件名需要过滤类名后缀Manager,所有的组件对象导出后都可以使用组件标签引用。在这里需要使用requireNativeComponent组件引入Native UI组件,如下所示:

JavaScript:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
    requireNativeComponent,
    AppRegistry,
    StyleSheet,
    View
} from 'react-native';/*
function requireNativeComponent(
    viewName: string,                         //构建的原生UI组件名称, 去掉manager后缀
    componentInterface?: ?ComponentInterface, //封装到哪个组件内部,可选值。一般将原生的UI组件二次封装成新的React组件时填写
    extraConfig?: ?{nativeOnly?: Object},     //额外配置,可选值
)
*/
const RCTMapView = requireNativeComponent('RCTMapView', null);

export default class ReactNativeCustomUIDemo extends Component {
    render() {
        return (
            <View style={styles.container}>
                <RCTMapView style={styles.mapView}/>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#F5FCFF',
    },
    mapView: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    }
});

AppRegistry.registerComponent('ReactNativeCustomUIDemo', () => ReactNativeCustomUIDemo);

3、UI组件属性

原生组件的属性桥接到JavaScript中使用,需要以标签形式就行访问。RN中提供了三个宏定义来桥接NativeUI的属性,分别如下:

//1.导出Native UI Property
//name:属性名称  type:该属性对应的类型
#define RCT_EXPORT_VIEW_PROPERTY(name, type)


//2.导出重映射的Native UI Property
//name:属性名称  keyPath:重映射属性名称  type:该属性对应的类型
#define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type)


//3.导出自定义的Native UI Property
//name:自定义的属性名称  type:该属性对应的类型  viewClass:该属性对应的组件
#define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass)

 默认情况下,JavaScript标签属性和Native属性相同,使用上述第1个宏导出属性即可。如果属性名称需要另外定义,则需要使用上述第2个宏导出属性。这两种宏定义的使用都必须满足JavaScript和OC之间的属性类型是支持转换的。同前面博文创建Native API组件的模块方法一样,属性的类型也支持标准JSON对象,RCTConvert类能够帮助实现类型的自动转换。如果当前属性的类型不支持转换,那么此时就要使用上述第3个宏导出属性。简单示例如下:

OC:

//  RCTMapViewManager.m
//  ReactNativeCustomUIDemo
//  Created by 夏远全 on 2020/3/9.
//  Copyright © 2020 Facebook. All rights reserved.

#import "RCTMapViewManager.h"
#import <MapKit/MapKit.h>

@implementation RCTMapViewManager

//导出模块类
RCT_EXPORT_MODULE();

//导出Native UI Property
//#define RCT_EXPORT_VIEW_PROPERTY(name, type)
//name:属性名称  type:该属性对应的类型
RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL);       //是否显示指南针
RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL);  //是否显示用户位置
RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL);         //是否显示比例尺

//返回Native UI
-(UIView *)view
{
  //地图
  MKMapView *mapView = [[MKMapView alloc] init];
  
  //样式
  mapView.mapType = MKMapTypeStandard;
  
  return mapView;
}

@end 

JavaScript:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, { Component } from 'react';
import {
    requireNativeComponent,
    AppRegistry,
    StyleSheet,
    View
} from 'react-native';

/*
function requireNativeComponent(
    viewName: string,                          //原生的UI组件名称
    componentInterface?: ?ComponentInterface,  //封装到哪个组件内部,可选值。一般将原生的UI组件二次封装成新的React组件时填写
    extraConfig?: ?{nativeOnly?: Object},      //额外配置,可选值
)
*/
const RCTMapView = requireNativeComponent('RCTMapView', null);

export default class ReactNativeCustomUIDemo extends Component {
    render() {
        return (
            <View style={styles.container}>
                <RCTMapView style={styles.mapView}
                            showsCompass={true}
                            showsUserLocation={true}
                            showsScale={true}
                />
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#F5FCFF',
    },
    mapView: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    }
});

AppRegistry.registerComponent('ReactNativeCustomUIDemo', () => ReactNativeCustomUIDemo); 

4、UI组件方法

NativeUI组件同样支持模块方法,也是使用RCT_EXPORT_METHOD宏定义,其方法定义中必须包含由JS传递出来的reactTag,其实现逻辑需要封装在RCTUIManager的addUIBlock接口的块函数中执行。在块函数中,可以通过RCTUIManager维护的ViewRegistry根据reactTag获得调用方法的组件实例。在JS中,需要为组件设置引用ref,调用方法时通过引用ReactNative.findNodeHandle(ref)来获取组件的reactTag,然后将其作为UI组件模块方法对应的参数传入。此处我将RCTMapView单独封装成一个独立的js文件,具体示例如下:

OC:

//  RCTMapViewManager.m
//  ReactNativeCustomUIDemo
//  Created by 夏远全 on 2020/3/9.
//  Copyright © 2020 Facebook. All rights reserved.

#import "RCTMapViewManager.h"
#import <React/RCTUIManager.h>
#import <MapKit/MapKit.h>

@implementation RCTMapViewManager

//导出模块类
RCT_EXPORT_MODULE();

//导出Native UI Property
//#define RCT_EXPORT_VIEW_PROPERTY(name, type)
//name:属性名称  type:该属性对应的类型
RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL);       //是否显示指南针
RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL);  //是否显示用户位置
RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL);         //是否显示比例尺

//导出方法
RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag){
  [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
        
        //根据reactTag取出对应的目标视图
        id view = viewRegistry[reactTag];
        if ([view isKindOfClass:MKMapView.class]) {
            //此处获取到了系统的MKMapView组件,可以调用MKMapView的内置方法
            // { code }
            printf("\n-----地图刷新了----\n");
        }
  }];
}

//返回Native UI
-(UIView *)view
{
  //地图
  MKMapView *mapView = [[MKMapView alloc] init];
  
  //样式
  mapView.mapType = MKMapTypeStandard;
  
  return mapView;
}

@end

JavaScript:

import React, { Component } from 'react';
import ReactNative, { requireNativeComponent, NativeModules ,StyleSheet } from 'react-native';//模块类(需要去掉前缀RCT)
const RCTMapViewManager = NativeModules.MapViewManager;

//UI组件
const RCTMapView = requireNativeComponent('RCTMapView', CustomMapView);

//引用
const RCT_UI_REF = "theMapView";

export default class CustomMapView extends Component{

    //方法调用
    componentDidMount(): void {
        //根据引用获取组件的reactTag作为reload方法的参数传入
        RCTMapViewManager.reload( ReactNative.findNodeHandle(this.refs[RCT_UI_REF]) );
    }

    render(){
        return (
            <RCTMapView ref={RCT_UI_REF}
                        style={styles.container}
                        showsCompass={true}
                        showsUserLocation={true}
                        showsScale={true}
            />
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    }
}); 

打印结果如下:

2020-03-19 16:55:59.671 [info][tid:main][RCTBatchedBridge.m:77] Initializing <RCTBatchedBridge: 0x60000143c380> (parent: <RCTBridge: 0x600000631490>, executor: RCTJSCExecutor)
2020-03-19 16:55:59.725 [warn][tid:com.facebook.react.JavaScript][RCTModuleData.mm:220] RCTBridge required dispatch_sync to load RCTDevSettings. This may lead to deadlocks
2020-03-19 16:55:59.891 [info][tid:main][RCTRootView.m:295] Running application ReactNativeCustomUIDemo ({
    initialProps =     {
    };
    rootTag = 1;
})
2020-03-19 16:55:59.893 [info][tid:com.facebook.react.JavaScript] Running application "ReactNativeCustomUIDemo" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF

-----地图刷新了----

5、UI组件事件

NativeUI组件也可以实现与JS进行事件交互,在ReactNative框架中,把原生的事件通知到JavaScript,最后由JavaScript端来完成事件的响应。在ReactNative中,还要在原生控件响应用户事件的地方,通过事件派发器RCTEventDispatcher的sendInputEventWithName方法来将事件发送给JavaScript模块。在ReactNative中,事件名会在Native模块中进行格式化处理,例如带有c/Change的事件名,会被自动转为JavaScript的onChange事件属性来响应。在RCTViewManager中,默认定义了一些事件,这些事件会自动与JavaScript标签中的onEventName属性进行绑定,如下所示:

//按压事件
press

//改变事件
change

//获得焦点事件
focus

//失去焦点事件
blur

//提交事件
submitEnding

//结束编辑
endEnding

//触摸开始
touchStart

//触摸移动
touchMove

//触摸取消
touchCancel

//触摸结束
touchEnd

以上都是系统内置事件属性,但是如果需要自定义的事件名,则需要在Manager类中重写-(NSArray *)customBubblingEventTypes接口实现。然后在JavaScript与OC中保持事件名一致即可。具体示例如下:

OC: 

//  RCTMapViewManager.m
//  ReactNativeCustomUIDemo
//  Created by 夏远全 on 2020/3/9.
//  Copyright © 2020 Facebook. All rights reserved.


#import "RCTMapViewManager.h"
#import <React/RCTUIManager.h>
#import <MapKit/MapKit.h>

@interface RCTMapViewManager() <MKMapViewDelegate>

@end

@implementation RCTMapViewManager

//导出模块类
RCT_EXPORT_MODULE();

//导出Native UI Property
//#define RCT_EXPORT_VIEW_PROPERTY(name, type)
//name:属性名称  type:该属性对应的类型
RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL);       //是否显示指南针
RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL);  //是否显示用户位置
RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL);         //是否显示比例尺

//导出方法
RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag){
  [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *,UIView *> *viewRegistry) {
        
        //根据reactTag取出对应的目标视图
        id view = viewRegistry[reactTag];
        if ([view isKindOfClass:MKMapView.class]) {
            //此处获取到了系统的MKMapView组件,可以调用MKMapView的内置方法
            // { code }
            printf("\n-----地图刷新了----\n");
        }
  }];
}

//返回Native UI
-(UIView *)view
{
  //地图
  MKMapView *mapView = [[MKMapView alloc] init];
  
  //样式
  mapView.mapType = MKMapTypeStandard;
  
  //代理
  mapView.delegate = self;
  
  return mapView;
}

//自定义事件名称
-(NSArray *)customBubblingEventTypes {
  return @[
          @"customEventHandler"
         ];
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

#pragma mark -  delegate
- (void)mapViewWillStartLoadingMap:(MKMapView *)mapView {
    NSDictionary *event = @{ @"target":mapView.reactTag,@"status":@"地图开始加载"};
    [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; //系统的change事件名称
}

- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView {
    NSDictionary *event = @{ @"target":mapView.reactTag,@"status":@"地图加载结束"};
    [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; //系统的change事件名称
}

- (void)mapViewWillStartRenderingMap:(MKMapView *)mapView {
    NSDictionary *event = @{ @"target":mapView.reactTag,@"status":@"mapViewWillStartRenderingMap"};
    [self.bridge.eventDispatcher sendInputEventWithName:@"customEventHandler" body:event]; //自定义的customEventHandler事件名称
}

#pragma clang diagnostic pop

@end

JavaScript:

import React, { Component } from 'react';
import ReactNative, { requireNativeComponent, NativeModules ,StyleSheet } from 'react-native';
// let NativeModules = require('NativeModules');

//模块类(需要去掉前缀RCT)
const RCTMapViewManager = NativeModules.MapViewManager;

//UI组件
const RCTMapView = requireNativeComponent('RCTMapView', CustomMapView);

//引用
const RCT_UI_REF = "theMapView";

export default class CustomMapView extends Component{

    //方法调用
    componentDidMount(): void {
        //根据引用获取组件的reactTag作为reload方法的参数传入
        RCTMapViewManager.reload( ReactNative.findNodeHandle(this.refs[RCT_UI_REF]) );
    }

    //系统事件
    systemEventComplete(body){
        console.log("body---------" + body.nativeEvent.status)
    }

    //自定义事件
    customEventComplete(body){
        console.log("body---------" + body.nativeEvent.status)
    }

    render(){
        return (
            <RCTMapView ref={RCT_UI_REF}
                        style={styles.container}
                        showsCompass={true}
                        showsUserLocation={true}
                        showsScale={true}
                        onChange={this.systemEventComplete.bind(this)}
                        onCustomEventHandler={this.customEventComplete.bind(this)}
            />
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    }
});

打印结果如下:

2020-03-20 11:13:49.770 [info][tid:main][RCTBatchedBridge.m:77] Initializing <RCTBatchedBridge: 0x600000558700> (parent: <RCTBridge: 0x60000175a220>, executor: RCTJSCExecutor)
2020-03-20 11:13:49.826 [warn][tid:com.facebook.react.JavaScript][RCTModuleData.mm:220] RCTBridge required dispatch_sync to load RCTDevSettings. This may lead to deadlocks
2020-03-20 11:13:50.461 [info][tid:main][RCTRootView.m:295] Running application ReactNativeCustomUIDemo ({
    initialProps =     {
    };
    rootTag = 1;
})
2020-03-20 11:13:50.463 [info][tid:com.facebook.react.JavaScript] Running application "ReactNativeCustomUIDemo" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF

-----地图刷新了----
2020-03-20 11:13:50.711 [info][tid:com.facebook.react.JavaScript] body---------mapViewWillStartRenderingMap
2020-03-20 11:13:50.792 [info][tid:com.facebook.react.JavaScript] body---------地图开始加载
2020-03-20 11:13:50.885 [info][tid:com.facebook.react.JavaScript] body---------地图加载结束

 

posted @ 2020-03-20 11:19  XYQ全哥  阅读(1273)  评论(0编辑  收藏  举报