首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Native UI Components

需要本机代码的项目

此页面仅适用于react-native init使用Create React Native App 制作的或使用此类应用程序弹出的项目。有关弹出的更多信息,请参阅创建React Native App存储库的指南

有许多原生UI小部件可以在最新的应用程序中使用 - 其中一些是该平台的一部分,另一些则作为第三方库提供,并且还可能在您自己的产品组合中使用。反应本土有几个最关键的平台组件已经包裹着,像ScrollViewTextInput,但不是所有的人,肯定不是你可能会自己写的前一个应用程序的。幸运的是,将这些现有组件与您的React Native应用程序无缝集成非常简单。

就像本地模块指南一样,这也是一个更高级的指南,假设你对iOS编程有些熟悉。本指南将向您展示如何构建本地UI组件,引导您完成MapView核心React Native库中现有组件的子集的实现。

iOS MapView示例

比方说,我们希望为我们的应用添加一个交互式地图 - 可以使用MKMapView,我们只需要使其可用于JavaScript。

原生视图由子类创建和操作RCTViewManager。这些子类在功能上与视图控制器类似,但基本上是单例 - 每个桥只创建一个实例。它们向本地视图公开本地视图,根据需要将视图RCTUIManager委托给它们以设置和更新视图的属性。该RCTViewManagers为通常还对意见的代表,通过桥事件发送回JavaScript的。

公开视图很简单:

  • RCTViewManager创建组件管理器的子类。
  • 添加RCT_EXPORT_MODULE()标记宏。
  • 实施该-(UIView *)view方法。
代码语言:javascript
复制
// RNTMapManager.m
#import <MapKit/MapKit.h>

#import <React/RCTViewManager.h>

@interface RNTMapManager : RCTViewManager
@end

@implementation RNTMapManager

RCT_EXPORT_MODULE()

- (UIView *)view
{
  return [[MKMapView alloc] init];
}

@end

注意:不要尝试在通过方法公开的实例上设置framebackgroundColor属性。React Native会覆盖您的自定义类设置的值,以匹配您的JavaScript组件的布局道具。如果你需要这个粒度的控制,可能会更好地包装你想在另一个样式中的实例,而不是返回包装。有关更多上下文,请参阅第2948期。UIView-viewUIViewUIViewUIView

在上面的例子中,我们在前面加上了我们的类名RNT。前缀用于避免与其他框架的名称冲突。Apple框架使用双字母前缀,React Native RCT用作前缀。为了避免名称冲突,我们建议使用除RCT您自己的类以外的三个字母的前缀。

那么你只需要一点JavaScript就可以使它成为一个可用的React组件:

代码语言:javascript
复制
// MapView.js

import { requireNativeComponent } from 'react-native';

// requireNativeComponent automatically resolves 'RNTMap' to 'RNTMapManager'
module.exports = requireNativeComponent('RNTMap', null);

// MyApp.js

import MapView from './MapView.js';

...

render() {
  return <MapView style={{ flex: 1 }} />;
}

请务必在RNTMap这里使用。我们希望在这里要求经理,这将公开我们的经理在Javascript中使用的视图。

注意:渲染时,不要忘记拉伸视图,否则你会盯着空白屏幕。

代码语言:javascript
复制
  render() {
    return <MapView style={{flex: 1}} />;
  }

现在,这是一个全功能的JavaScript本地地图视图组件,通过捏缩放和其他本地手势支持完成。我们无法真正从JavaScript控制它,尽管:(

属性

我们可以做的第一件事是让这个组件更加可用,这是为了弥合一些本地属性。假设我们希望能够禁用缩放并指定可见区域。禁用缩放是一个简单的布尔值,所以我们添加这一行:

代码语言:javascript
复制
// RNTMapManager.m
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)

请注意,我们明确指定类型为BOOL- React Native RCTConvert在引擎盖下通过桥接器转换各种不同的数据类型,而错误的值将显示方便的“RedBox”错误,以便您尽快知道存在问题。当事情如此简单时,整个实现将由这个宏来为您处理。

现在为了实际禁用缩放,我们在JS中设置属性:

代码语言:javascript
复制
// MyApp.js
<MapView 
  zoomEnabled={false} 
  style={{ flex: 1 }}
/>;

要记录我们的MapView组件的属性(以及它们接受哪些值),我们将添加一个包装组件并用React记录界面PropTypes

代码语言:javascript
复制
// MapView.js
import PropTypes from 'prop-types';
import React from 'react';
import { requireNativeComponent } from 'react-native';

class MapView extends React.Component {
  render() {
    return <RNTMap {...this.props} />;
  }
}

MapView.propTypes = {
  /**
   * A Boolean value that determines whether the user may use pinch 
   * gestures to zoom in and out of the map.
   */
  zoomEnabled: PropTypes.bool,
};

var RNTMap = requireNativeComponent('RNTMap', MapView);

module.exports = MapView;

现在我们有一个很好的记录包装组件,很容易处理。请注意,我们改变了第二个参数requireNativeComponentnull到新的MapView包装组件。这允许基础结构验证propTypes与本地道具相匹配,以减少ObjC和JS代码之间不匹配的可能性。

接下来,让我们添加更复杂的region道具。我们从添加本机代码开始:

代码语言:javascript
复制
// RNTMapManager.m
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
  [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}

好的,这比BOOL我们以前的简单情况更复杂。现在我们有一个MKCoordinateRegion需要转换函数的类型,并且我们有自定义代码,这样当我们从JS设置区域时,视图将会动画。在我们提供的函数体内,json指的是从JS传递的原始值。还有一个view变量可以让我们访问管理器的视图实例,defaultView如果JS向我们发送一个空的标记,我们使用该变量将属性重置为默认值。

你可以为你的视图编写任何你想要的转换函数 - 这里是MKCoordinateRegion通过一个类别的实现RCTConvert。它使用一个已经存在的ReactNative类别RCTConvert+CoreLocation

代码语言:javascript
复制
// RNTMapManager.m

#import "RCTConvert+Mapkit.m"

// RCTConvert+Mapkit.h

#import <MapKit/MapKit.h>
#import <React/RCTConvert.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTConvert+CoreLocation.h>

@interface RCTConvert (Mapkit)

+ (MKCoordinateSpan)MKCoordinateSpan:(id)json;
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json;

@end

@implementation RCTConvert(MapKit)

+ (MKCoordinateSpan)MKCoordinateSpan:(id)json
{
  json = [self NSDictionary:json];
  return (MKCoordinateSpan){
    [self CLLocationDegrees:json[@"latitudeDelta"]],
    [self CLLocationDegrees:json[@"longitudeDelta"]]
  };
}

+ (MKCoordinateRegion)MKCoordinateRegion:(id)json
{
  return (MKCoordinateRegion){
    [self CLLocationCoordinate2D:json],
    [self MKCoordinateSpan:json]
  };
}

@end

这些转换函数的设计目的是通过显示“RedBox”错误,并在遇到丢失键或其他开发人员错误时返回标准初始化值,从而安全地处理JS可能抛出的任何JSON。

为了完成对regionprop的支持,我们需要将它记录在文件中propTypes(否则我们会得到一个本地prop没有记录的错误),那么我们可以像设置任何其他prop一样设置它:

代码语言:javascript
复制
// MapView.js

MapView.propTypes = {
  /**
   * A Boolean value that determines whether the user may use pinch 
   * gestures to zoom in and out of the map.
   */
  zoomEnabled: PropTypes.bool,

  /**
   * The region to be displayed by the map.
   *
   * The region is defined by the center coordinates and the span of
   * coordinates to display.
   */
  region: PropTypes.shape({
    /**
     * Coordinates for the center of the map.
     */
    latitude: PropTypes.number.isRequired,
    longitude: PropTypes.number.isRequired,

    /**
     * Distance between the minimum and the maximum latitude/longitude
     * to be displayed.
     */
    latitudeDelta: PropTypes.number.isRequired,
    longitudeDelta: PropTypes.number.isRequired,
  }),
};

// MyApp.js

render() {
  var region = {
    latitude: 37.48,
    longitude: -122.16,
    latitudeDelta: 0.1,
    longitudeDelta: 0.1,
  };
  return (
    <MapView 
      region={region} 
      zoomEnabled={false}
      style={{ flex: 1 }}
    />
  );
}

在这里你可以看到,该区域的形状在JS文档中是明确的 - 理想情况下,我们可以对这些东西进行编码,但这还没有发生。

有时,您的本地组件会拥有一些您不希望它们成为相关React组件的API的一部分的特殊属性。例如,为原始本机事件创建Switch了一个自定义onChange处理程序,并公开一个onValueChange仅使用布尔值而非原始事件调用的处理程序属性。既然你不希望这些本地唯一的属性成为API的一部分,你不想把它们放入propTypes,但如果你不这样做,你会得到一个错误。解决方案只是将它们添加到nativeOnly选项中,例如

代码语言:javascript
复制
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {
  nativeOnly: { onChange: true }
});

活动

所以现在我们有一个本地地图组件,我们可以从JS轻松控制,但我们如何处理来自用户的事件,如捏缩放或平移以更改可见区域?

到目前为止,我们刚刚MKMapView从经理的-(UIView *)view方法中返回了一个实例。我们不能添加新的属性,MKMapView所以我们必须创建一个新的子类MKMapView,我们用它来查看。然后我们可以onRegionChange在这个子类上添加一个回调函数:

代码语言:javascript
复制
// RNTMapView.h

#import <MapKit/MapKit.h>

#import <React/RCTComponent.h>

@interface RNTMapView: MKMapView

@property (nonatomic, copy) RCTBubblingEventBlock onRegionChange;

@end

// RNTMapView.m

#import "RNTMapView.h"

@implementation RNTMapView

@end

接下来,声明一个事件处理程序属性RNTMapManager,使其成为它公开的所有视图的委托,并通过从本地视图调用事件处理程序块将事件转发给JS。

代码语言:javascript
复制
// RNTMapManager.m

#import <MapKit/MapKit.h>
#import <React/RCTViewManager.h>

#import "RNTMapView.h"
#import "RCTConvert+Mapkit.m"

@interface RNTMapManager : RCTViewManager <MKMapViewDelegate>
@end

@implementation RNTMapManager

RCT_EXPORT_MODULE()

RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTBubblingEventBlock)

RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, MKMapView)
{
    [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}

- (UIView *)view
{
  RNTMapView *map = [RNTMapView new];
  map.delegate = self;
  return map;
}

#pragma mark MKMapViewDelegate

- (void)mapView:(RNTMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
  if (!mapView.onRegionChange) {
    return;
  }

  MKCoordinateRegion region = mapView.region;
  mapView.onRegionChange(@{
    @"region": @{
      @"latitude": @(region.center.latitude),
      @"longitude": @(region.center.longitude),
      @"latitudeDelta": @(region.span.latitudeDelta),
      @"longitudeDelta": @(region.span.longitudeDelta),
    }
  });
}
@end

在委托方法中-mapView:regionDidChangeAnimated:,使用区域数据在相应的视图上调用事件处理程序块。调用onRegionChange事件处理程序块会导致在JavaScript中调用相同的回调支持。这个回调函数与原始事件一起被调用,我们通常在包装器组件中处理这个事件来创建一个更简单的API:

代码语言:javascript
复制
// MapView.js

class MapView extends React.Component {
  _onRegionChange = (event) => {
    if (!this.props.onRegionChange) {
      return;
    }

    // process raw event... 
    this.props.onRegionChange(event.nativeEvent);
  }
  render() {
    return (
      <RNTMap 
        {...this.props} 
        onRegionChange={this._onRegionChange} 
      />
    );
  }
}
MapView.propTypes = {
  /**
   * Callback that is called continuously when the user is dragging the map.
   */
  onRegionChange: PropTypes.func,
  ...
};

// MyApp.js

class MyApp extends React.Component {
  onRegionChange(event) {
    // Do stuff with event.region.latitude, etc.
  }

  render() {
    var region = {
      latitude: 37.48,
      longitude: -122.16,
      latitudeDelta: 0.1,
      longitudeDelta: 0.1,
    };
    return (
      <MapView
        region={region} 
        zoomEnabled={false} 
        onRegionChange={this.onRegionChange}
      />
    );
  }  
}

样式

由于我们所有的原生反应视图都是子类UIView,因此大多数样式属性都可以像您期望的那样工作。有些组件需要一个默认的样式,但是,例如UIDatePicker,这是一个固定的大小。此默认样式对于布局算法按预期工作很重要,但我们也希望能够在使用组件时覆盖默认样式。DatePickerIOS通过将本地组件包装在具有灵活样式的额外视图中,并在内部本地组件上使用固定样式(使用本机传入的常量生成)执行此操作:

代码语言:javascript
复制
// DatePickerIOS.ios.js

import { UIManager } from 'react-native';
var RCTDatePickerIOSConsts = UIManager.RCTDatePicker.Constants;
...
  render: function() {
    return (
      <View style={this.props.style}>
        <RCTDatePickerIOS
          ref={DATEPICKER}
          style={styles.rkDatePickerIOS}
          ...
        />
      </View>
    );
  }
});

var styles = StyleSheet.create({
  rkDatePickerIOS: {
    height: RCTDatePickerIOSConsts.ComponentHeight,
    width: RCTDatePickerIOSConsts.ComponentWidth,
  },
});

所述RCTDatePickerIOSConsts常数由敛像这样的天然组分的实际帧从天然导出:

代码语言:javascript
复制
// RCTDatePickerManager.m

- (NSDictionary *)constantsToExport
{
  UIDatePicker *dp = [[UIDatePicker alloc] init];
  [dp layoutIfNeeded];

  return @{
    @"ComponentHeight": @(CGRectGetHeight(dp.frame)),
    @"ComponentWidth": @(CGRectGetWidth(dp.frame)),
    @"DatePickerModes": @{
      @"time": @(UIDatePickerModeTime),
      @"date": @(UIDatePickerModeDate),
      @"datetime": @(UIDatePickerModeDateAndTime),
    }
  };
}

本指南涵盖了桥接自定义本机组件的许多方面,但您可能需要考虑更多内容,例如用于插入和布置子视图的自定义挂钩。如果你想更深入,请查看一些实现组件的源代码

扫码关注腾讯云开发者

领取腾讯云代金券

http://www.vxiaotou.com