React Native使用指南-原生UI组件
在如今的App中,已經有成千上萬的原生UI部件了——其中的一些是平臺的一部分,另一些可能來自于一些第三方庫,而且可能你自己還收藏了很多。React Native已經封裝了大部分最常見的組件,譬如ScrollView和TextInput,但不可能封裝全部組件。而且,說不定你曾經為自己以前的App還封裝過一些組件,React Native肯定沒法包含它們。幸運的是,在React Naitve應用程序中封裝和植入已有的組件非常簡單。
和原生模塊向導一樣,本向導也是一個相對高級的向導,我們假設你已經對iOS編程頗有經驗。本向導會引導你如何構建一個原生UI組件,帶領你了解React Native核心庫中MapView組件的具體實現。
iOS MapView樣例
假設我們要把地圖組件植入到我們的App中——我們用到的是MKMapView,而現在只需要讓它可以被Javascript重用。
原生視圖都需要被一個RCTViewManager的子類來創建和管理。這些管理器在功能上有些類似“視圖控制器”,但它們本質上都是單例 - React Native只會為每個管理器創建一個實例。它們創建原生的視圖并提供給RCTUIManager,RCTUIManager則會反過來委托它們在需要的時候去設置和更新視圖的屬性。RCTViewManager還會代理視圖的所有委托,并給JavaScript發回對應的事件。
提供原生視圖很簡單:
- 首先創建一個子類
- 添加RCT_EXPORT_MODULE()標記宏
- 實現-(UIView *)view方法
接下來你需要一些Javascript代碼來讓這個視圖變成一個可用的React組件:
// MapView.jsvar { requireNativeComponent } = require('react-native');// requireNativeComponent 自動把這個組件提供給 "RCTMapManager" module.exports = requireNativeComponent('RCTMap', null);現在我們就已經實現了一個完整功能的地圖組件了,諸如捏放和其它的手勢都已經完整支持。但是現在我們還不能真正的從Javascript端控制它。(╯﹏╰)
屬性
我們能讓這個組件變得更強大的第一件事情就是要能夠封裝一些原生屬性供Javascript使用。舉例來說,我們希望能夠禁用手指捏放操作,然后指定一個初始的地圖可見區域。禁用捏放操作只需要一個布爾值類型的屬性就行了,所以我們添加這么一行:
// RCTMapManager.m RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)注意我們現在把類型聲明為BOOL類型——React Native用RCTConvert來在JavaScript和原生代碼之間完成類型轉換。如果轉換無法完成,會產生一個“紅屏”的報錯提示,這樣你就能立即知道代碼中出現了問題。如果一切進展順利,上面這個宏就已經包含了導出屬性的全部實現。
現在要想禁用捏放操作,我們只需要在JS里設置對應的屬性:
// MyApp.js <MapView pitchEnabled={false} />但這樣并不能很好的說明這個組件的用法——用戶要想知道我們的組件有哪些屬性可以用,以及可以取什么樣的值,他不得不一路翻到Objective-C的代碼。要解決這個問題,我們可以創建一個封裝組件,并且通過PropTypes來說明這個組件的接口。
// MapView.js var React = require('react-native'); var { requireNativeComponent } = React;class MapView extends React.Component {render() {return <RCTMap {...this.props} />;} }MapView.propTypes = {/*** 當這個屬性被設置為true,并且地圖上綁定了一個有效的可視區域的情況下,* 可以通過捏放操作來改變攝像頭的偏轉角度。* 當這個屬性被設置成false時,攝像頭的角度會被忽略,地圖會一直顯示為俯視狀態。*/pitchEnabled: React.PropTypes.bool, };var RCTMap = requireNativeComponent('RCTMap', MapView);module.exports = MapView;譯注:使用了封裝組件之后,你還需要注意到module.exports導出的不再是requireNativeComponent的返回值,而是所創建的包裝組件。
現在我們有了一個封裝好的組件,還有了一些注釋文檔,用戶使用起來也更方便了。注意我們現在把requireNativeComponent的第二個參數從null變成了用于封裝的組件MapView。這使得React Native的底層框架可以檢查原生屬性和包裝類的屬性是否一致,來減少出現問題的可能。
現在,讓我們添加一個更復雜些的region屬性。我們首先添加原生代碼:
// RCTMapManager.m RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap) {[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES]; }這段代碼比剛才的一個簡單的BOOL要復雜的多了。現在我們多了一個需要做類型轉換的MKCoordinateRegion類型,還添加了一部分自定義的代碼,這樣當我們在JS里改變地圖的可視區域的時候,視角會平滑地移動過去。在我們提供的函數體內,json代表了JS中傳遞的尚未解析的原始值。函數里還有一個view變量,使得我們可以訪問到對應的視圖實例。最后,還有一個defaultView對象,這樣當JS給我們發送null的時候,可以把視圖的這個屬性重置回默認值。
你可以為視圖編寫任何你所需要的轉換函數——下面就是MKCoordinateRegion的轉換實現,它通過兩個RCTConvert的擴展來完成:
@implementation RCTConvert(CoreLocation)RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue); RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue);+ (CLLocationCoordinate2D)CLLocationCoordinate2D:(id)json {json = [self NSDictionary:json];return (CLLocationCoordinate2D){[self CLLocationDegrees:json[@"latitude"]],[self CLLocationDegrees:json[@"longitude"]]}; }@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]}; }這些轉換函數被設計為可以安全的處理任何JS扔過來的JSON:當有任何缺少的鍵或者其它問題發生的時候,顯示一個“紅屏”的錯誤提示。
為了完成region屬性的支持,我們還需要在propTypes里添加相應的說明(否則我們會立刻收到一個錯誤提示),然后就可以像使用其他屬性一樣使用了:
// MapView.jsMapView.propTypes = {/*** 當這個屬性被設置為true,并且地圖上綁定了一個有效的可視區域的情況下,* 可以通過捏放操作來改變攝像頭的偏轉角度。* 當這個屬性被設置成false時,攝像頭的角度會被忽略,地圖會一直顯示為俯視狀態。*/pitchEnabled: React.PropTypes.bool,/*** 地圖要顯示的區域。** 區域由中心點坐標和區域范圍坐標來定義。* */region: React.PropTypes.shape({/*** 地圖中心點的坐標。*/latitude: React.PropTypes.number.isRequired,longitude: React.PropTypes.number.isRequired,/*** 最小/最大經、緯度間的距離。*/latitudeDelta: React.PropTypes.number.isRequired,longitudeDelta: React.PropTypes.number.isRequired,}), };// MyApp.jsrender() {var region = {latitude: 37.48,longitude: -122.16,latitudeDelta: 0.1,longitudeDelta: 0.1,};return <MapView region={region} />;}現在你可以看到region屬性的整個結構已經加上了文檔說明——將來可能我們會自動生成一些類似的代碼,但目前還沒有這樣的手段。
有時候你的原生組件有一些特殊的屬性希望導出,但并不希望它成為公開的接口。舉個例子,Switch組件可能會有一個onChange屬性用來傳遞原始的原生事件,然后導出一個onValueChange屬性,這個屬性在調用的時候會帶上Switch的狀態作為參數之一。這樣的話你可能不希望原生專用的屬性出現在API之中,也就不希望把它放到propTypes里。可是如果你不放的話,又會出現一個報錯。解決方案就是帶上額外的nativeOnly參數,像這樣:
var RCTSwitch = requireNativeComponent('RCTSwitch', Switch, {nativeOnly: { onChange: true } });事件
現在我們已經有了一個原生地圖組件,并且從JS可以很容易的控制它了。不過我們怎么才能處理來自用戶的事件,譬如縮放操作或者拖動來改變可視區域?關鍵的步驟就在于讓RCTMapManager來委托我們提供的所有視圖,然后把事件通過分發器傳遞給JavaScript。最終的代碼看起來類似這樣(比起完整的實現有所簡化):
// RCTMapManager.m#import "RCTMapManager.h"#import <MapKit/MapKit.h>#import "RCTBridge.h" #import "RCTEventDispatcher.h" #import "UIView+React.h"@interface RCTMapManager() <MKMapViewDelegate> @end@implementation RCTMapManagerRCT_EXPORT_MODULE()- (UIView *)view {MKMapView *map = [[MKMapView alloc] init];map.delegate = self;return map; }#pragma mark MKMapViewDelegate- (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated {MKCoordinateRegion region = mapView.region;NSDictionary *event = @{@"target": mapView.reactTag,@"region": @{@"latitude": @(region.center.latitude),@"longitude": @(region.center.longitude),@"latitudeDelta": @(region.span.latitudeDelta),@"longitudeDelta": @(region.span.longitudeDelta),}};[self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; }如你所見,我們剛才配置了管理器,委托它代理創建的所有視圖,并且在委托方法-mapView:regionDidChangeAnimated:中,把地圖目前的區域以及reactTag目標封裝成了一個事件,這樣我們的事件就可以通過sendInputEventWithName:body:發送到正確的React組件實例上。事件名@"topChange"對應的是JavaScript端的onChange回調屬性。這個回調會被原生事件執行,然后我們通常都會在封裝組件里做一些處理,來使得API更簡明:
// MapView.jsclass MapView extends React.Component {constructor() {this._onChange = this._onChange.bind(this);}_onChange(event: Event) {if (!this.props.onRegionChange) {return;}this.props.onRegionChange(event.nativeEvent.region);}render() {return <RCTMap {...this.props} onChange={this._onChange} />;} } MapView.propTypes = {/*** Callback that is called continuously when the user is dragging the map.*/onRegionChange: React.PropTypes.func,... };樣式
因為我們所有的視圖都是UIView的子類,大部分的樣式屬性應該直接就可以生效。但有一部分組件會希望使用自己定義的默認樣式,例如UIDatePicker希望自己的大小是固定的。這個默認屬性對于布局算法的正常工作來說很重要,但我們也希望在使用這個組件的時候可以覆蓋這些默認的樣式。DatePickerIOS實現這個功能的辦法是通過封裝一個擁有彈性樣式的額外視圖,然后在內層的視圖上應用一個固定樣式(通過原生傳遞來的常數生成):
// DatePickerIOS.ios.jsvar RCTDatePickerIOSConsts = require('react-native').UIManager.RCTDatePicker.Constants; ...render: function() {return (<View style={this.props.style}><RCTDatePickerIOSref={DATEPICKER}style={styles.rkDatePickerIOS}.../></View>);} });var styles = StyleSheet.create({rkDatePickerIOS: {height: RCTDatePickerIOSConsts.ComponentHeight,width: RCTDatePickerIOSConsts.ComponentWidth,}, });常量RCTDatePickerIOSConsts在原生代碼中導出,從一個組件的實際布局上獲取到:
// 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),}}; }本向導覆蓋了包裝原生組件所需了解的許多方面,不過你可能還有很多知識需要了解,譬如特殊的方式來插入和布局子視圖。如果你想更深入了解,可以閱讀RCTMapManager和其它的組件的源代碼。
總結
以上是生活随笔為你收集整理的React Native使用指南-原生UI组件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于iOS7里的JavaScriptCo
- 下一篇: web处理高并发措施