• 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("
    -----地图刷新了----
    ");
            }
      }];
    }
    
    //返回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("
    -----地图刷新了----
    ");
            }
      }];
    }
    
    //返回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---------地图加载结束
  • 相关阅读:
    使用 IntraWeb (40) 自定义 Session 数据
    原生时代 来看看十年前李彦宏、马化腾和马云对云计算的评价
    .NET 在云原生时代的蜕变,让我在云时代脱颖而出
    Docker多阶段构建实战(multistage builds)
    使用.NET Core 构建现代化的桌面应用
    Mac添加自定义启动图标到Launchpad
    CentOS 7创建自定义KVM模板(现有KVM迁移到另外一台机)
    [ASP.NET Core MVC] 如何实现运行时动态定义Controller类型?
    [ASP.NET Core 3框架揭秘] 配置[9]:自定义配置源
    [AWS] Export and Import Data from DynamoDB Table 从 DynamoDB 数据库中导入和导出数据
  • 【推广】 阿里云小站-上云优惠聚集地(新老客户同享)更有每天限时秒杀!
    【推广】 云服务器低至0.95折 1核2G ECS云服务器8.1元/月
    【推广】 阿里云老用户升级四重礼遇享6.5折限时折扣!
  • 原文地址:https://www.cnblogs.com/XYQ-208910/p/12455916.html
走看看 - 开发者的网上家园