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---------地圖加載結束

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM