diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js
index 57201757475..9a7e2e1aab2 100644
--- a/Examples/UIExplorer/MapViewExample.js
+++ b/Examples/UIExplorer/MapViewExample.js
@@ -149,9 +149,6 @@ var MapViewExample = React.createClass({
getInitialState() {
return {
- mapRegion: null,
- mapRegionInput: null,
- annotations: null,
isFirstLoad: true,
};
},
@@ -163,12 +160,12 @@ var MapViewExample = React.createClass({
style={styles.map}
onRegionChange={this._onRegionChange}
onRegionChangeComplete={this._onRegionChangeComplete}
- region={this.state.mapRegion || undefined}
- annotations={this.state.annotations || undefined}
+ region={this.state.mapRegion}
+ annotations={this.state.annotations}
/>
);
@@ -208,6 +205,114 @@ var MapViewExample = React.createClass({
});
+var CalloutMapViewExample = React.createClass({
+
+ getInitialState() {
+ return {
+ isFirstLoad: true,
+ };
+ },
+
+ render() {
+ if (this.state.isFirstLoad) {
+ var onRegionChangeComplete = (region) => {
+ this.setState({
+ isFirstLoad: false,
+ annotations: [{
+ longitude: region.longitude,
+ latitude: region.latitude,
+ title: 'More Info...',
+ hasRightCallout: true,
+ onRightCalloutPress: () => {
+ alert('You Are Here');
+ },
+ }],
+ });
+ };
+ }
+
+ return (
+
+ );
+ },
+
+});
+
+var CustomPinColorMapViewExample = React.createClass({
+
+ getInitialState() {
+ return {
+ isFirstLoad: true,
+ };
+ },
+
+ render() {
+ if (this.state.isFirstLoad) {
+ var onRegionChangeComplete = (region) => {
+ this.setState({
+ isFirstLoad: false,
+ annotations: [{
+ longitude: region.longitude,
+ latitude: region.latitude,
+ title: 'You Are Purple',
+ tintColor: MapView.PinColors.PURPLE,
+ }],
+ });
+ };
+ }
+
+ return (
+
+ );
+ },
+
+});
+
+var CustomPinImageMapViewExample = React.createClass({
+
+ getInitialState() {
+ return {
+ isFirstLoad: true,
+ };
+ },
+
+ render() {
+ if (this.state.isFirstLoad) {
+ var onRegionChangeComplete = (region) => {
+ this.setState({
+ isFirstLoad: false,
+ annotations: [{
+ longitude: region.longitude,
+ latitude: region.latitude,
+ title: 'Thumbs Up!',
+ image: require('image!uie_thumb_big'),
+ }],
+ });
+ };
+ }
+
+ return (
+
+ );
+ },
+
+});
+
var styles = StyleSheet.create({
map: {
height: 150,
@@ -249,5 +354,23 @@ exports.examples = [
render() {
return ;
}
- }
+ },
+ {
+ title: 'Callout example',
+ render() {
+ return ;
+ }
+ },
+ {
+ title: 'Custom pin color',
+ render() {
+ return ;
+ }
+ },
+ {
+ title: 'Custom pin image',
+ render() {
+ return ;
+ }
+ },
];
diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js
index 58950c49071..26cd3f61ee6 100644
--- a/Libraries/Components/MapView/MapView.js
+++ b/Libraries/Components/MapView/MapView.js
@@ -12,7 +12,9 @@
'use strict';
var EdgeInsetsPropType = require('EdgeInsetsPropType');
+var Image = require('Image');
var NativeMethodsMixin = require('NativeMethodsMixin');
+var PinColors = require('NativeModules').UIManager.RCTMap.Constants.PinColors;
var Platform = require('Platform');
var React = require('React');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
@@ -21,6 +23,8 @@ var View = require('View');
var deepDiffer = require('deepDiffer');
var insetsDiffer = require('insetsDiffer');
var merge = require('merge');
+var processColor = require('processColor');
+var resolveAssetSource = require('resolveAssetSource');
var requireNativeComponent = require('requireNativeComponent');
type Event = Object;
@@ -194,7 +198,21 @@ var MapView = React.createClass({
/**
* annotation id
*/
- id: React.PropTypes.string
+ id: React.PropTypes.string,
+
+ /**
+ * The pin color. This can be any valid color string, or you can use one
+ * of the predefined PinColors constants. Applies to both standard pins
+ * and custom pin images.
+ * @platform ios
+ */
+ tintColor: React.PropTypes.string,
+
+ /**
+ * Custom pin image. This must be a static image resource inside the app.
+ * @platform ios
+ */
+ image: Image.propTypes.source,
})),
@@ -235,47 +253,79 @@ var MapView = React.createClass({
active: React.PropTypes.bool,
},
- _onChange: function(event: Event) {
- if (event.nativeEvent.continuous) {
- this.props.onRegionChange &&
- this.props.onRegionChange(event.nativeEvent.region);
- } else {
- this.props.onRegionChangeComplete &&
- this.props.onRegionChangeComplete(event.nativeEvent.region);
- }
- },
+ render: function() {
- _onPress: function(event: Event) {
- if (event.nativeEvent.action === 'annotation-click') {
- this.props.onAnnotationPress && this.props.onAnnotationPress(event.nativeEvent.annotation);
- }
+ let {annotations} = this.props;
+ annotations = annotations && annotations.map((annotation: Object) => {
+ let {tintColor, image} = annotation;
+ return {
+ ...annotation,
+ tintColor: tintColor && processColor(tintColor),
+ image: image && resolveAssetSource(image),
+ };
+ });
- if (event.nativeEvent.action === 'callout-click') {
- if (!this.props.annotations) {
- return;
- }
-
- // Find the annotation with the id of what has been pressed
- for (var i = 0; i < this.props.annotations.length; i++) {
- var annotation = this.props.annotations[i];
- if (annotation.id === event.nativeEvent.annotationId) {
- // Pass the right function
- if (event.nativeEvent.side === 'left') {
- annotation.onLeftCalloutPress && annotation.onLeftCalloutPress(event.nativeEvent);
- } else if (event.nativeEvent.side === 'right') {
- annotation.onRightCalloutPress && annotation.onRightCalloutPress(event.nativeEvent);
+ // TODO: these should be separate events, to reduce bridge traffic
+ if (annotations || this.props.onAnnotationPress) {
+ var onPress = (event: Event) => {
+ if (event.nativeEvent.action === 'annotation-click') {
+ this.props.onAnnotationPress &&
+ this.props.onAnnotationPress(event.nativeEvent.annotation);
+ } else if (event.nativeEvent.action === 'callout-click') {
+ // Find the annotation with the id that was pressed
+ for (let i = 0, l = annotations.length; i < l; i++) {
+ let annotation = annotations[i];
+ if (annotation.id === event.nativeEvent.annotationId) {
+ // Pass the right function
+ if (event.nativeEvent.side === 'left') {
+ annotation.onLeftCalloutPress &&
+ annotation.onLeftCalloutPress(event.nativeEvent);
+ } else if (event.nativeEvent.side === 'right') {
+ annotation.onRightCalloutPress &&
+ annotation.onRightCalloutPress(event.nativeEvent);
+ }
+ break;
+ }
}
}
- }
-
+ };
}
- },
- render: function() {
- return ;
+ // TODO: these should be separate events, to reduce bridge traffic
+ if (this.props.onRegionChange || this.props.onRegionChangeComplete) {
+ var onChange = (event: Event) => {
+ if (event.nativeEvent.continuous) {
+ this.props.onRegionChange &&
+ this.props.onRegionChange(event.nativeEvent.region);
+ } else {
+ this.props.onRegionChangeComplete &&
+ this.props.onRegionChangeComplete(event.nativeEvent.region);
+ }
+ };
+ }
+
+ return (
+
+ );
},
});
+/**
+ * Standard iOS MapView pin color constants, to be used with the
+ * `annotation.tintColor` property. You are not obliged to use these,
+ * but they are useful for matching the standard iOS look and feel.
+ */
+MapView.PinColors = PinColors && {
+ RED: PinColors.RED,
+ GREEN: PinColors.GREEN,
+ PURPLE: PinColors.PURPLE,
+};
+
var RCTMap = requireNativeComponent('RCTMap', MapView, {
nativeOnly: {onChange: true, onPress: true}
});
diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m
index 53c6b9cad22..8fc14e102fb 100644
--- a/React/Base/RCTConvert.m
+++ b/React/Base/RCTConvert.m
@@ -397,6 +397,9 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
+ (UIColor *)UIColor:(id)json
{
+ if (!json) {
+ return nil;
+ }
if ([json isKindOfClass:[NSArray class]]) {
NSArray *components = [self NSNumberArray:json];
CGFloat alpha = components.count > 3 ? [self CGFloat:components[3]] : 1.0;
@@ -404,13 +407,16 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
green:[self CGFloat:components[1]]
blue:[self CGFloat:components[2]]
alpha:alpha];
- } else {
+ } else if ([json isKindOfClass:[NSNumber class]]) {
NSUInteger argb = [self NSUInteger:json];
CGFloat a = ((argb >> 24) & 0xFF) / 255.0;
CGFloat r = ((argb >> 16) & 0xFF) / 255.0;
CGFloat g = ((argb >> 8) & 0xFF) / 255.0;
CGFloat b = (argb & 0xFF) / 255.0;
return [UIColor colorWithRed:r green:g blue:b alpha:a];
+ } else {
+ RCTLogConvertError(json, @"a color");
+ return nil;
}
}
@@ -460,12 +466,9 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
NSURL *URL = [self NSURL:path];
NSString *scheme = URL.scheme.lowercaseString;
if ([scheme isEqualToString:@"file"]) {
- if (RCTIsXCAssetURL(URL)) {
- // Image may reside inside a .car file, in which case we have no choice
- // but to use +[UIImage imageNamed] - but this method isn't thread safe
- NSString *assetName = RCTBundlePathForURL(URL);
- image = [UIImage imageNamed:assetName];
- } else {
+ NSString *assetName = RCTBundlePathForURL(URL);
+ image = [UIImage imageNamed:assetName];
+ if (!image) {
// Attempt to load from the file system
NSString *filePath = URL.path;
if (filePath.pathExtension.length == 0) {
diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h
index e138da40531..e8f3781a6c6 100644
--- a/React/Base/RCTUtils.h
+++ b/React/Base/RCTUtils.h
@@ -84,6 +84,9 @@ RCT_EXTERN NSError *RCTErrorWithMessage(NSString *message);
RCT_EXTERN id RCTNilIfNull(id value);
RCT_EXTERN id RCTNullIfNil(id value);
+// Convert NaN or infinite values to zero, as these aren't JSON-safe
+RCT_EXTERN double RCTZeroIfNaN(double value);
+
// Convert data to a Base64-encoded data URL
RCT_EXTERN NSURL *RCTDataURL(NSString *mimeType, NSData *data);
@@ -96,3 +99,6 @@ RCT_EXTERN NSString *RCTBundlePathForURL(NSURL *URL);
// Determines if a given image URL actually refers to an XCAsset
RCT_EXTERN BOOL RCTIsXCAssetURL(NSURL *imageURL);
+
+// Converts a CGColor to a hex string
+RCT_EXTERN NSString *RCTColorToHexString(CGColorRef color);
diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m
index 8d6f3238d0c..b7a75148837 100644
--- a/React/Base/RCTUtils.m
+++ b/React/Base/RCTUtils.m
@@ -420,6 +420,11 @@ id RCTNilIfNull(id value)
return value == (id)kCFNull ? nil : value;
}
+RCT_EXTERN double RCTZeroIfNaN(double value)
+{
+ return isnan(value) || isinf(value) ? 0 : value;
+}
+
NSURL *RCTDataURL(NSString *mimeType, NSData *data)
{
return [NSURL URLWithString:
@@ -511,3 +516,62 @@ BOOL RCTIsXCAssetURL(NSURL *imageURL)
}
return YES;
}
+
+static void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4])
+{
+ CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(color));
+ const CGFloat *components = CGColorGetComponents(color);
+ switch (model)
+ {
+ case kCGColorSpaceModelMonochrome:
+ {
+ rgba[0] = components[0];
+ rgba[1] = components[0];
+ rgba[2] = components[0];
+ rgba[3] = components[1];
+ break;
+ }
+ case kCGColorSpaceModelRGB:
+ {
+ rgba[0] = components[0];
+ rgba[1] = components[1];
+ rgba[2] = components[2];
+ rgba[3] = components[3];
+ break;
+ }
+ case kCGColorSpaceModelCMYK:
+ case kCGColorSpaceModelDeviceN:
+ case kCGColorSpaceModelIndexed:
+ case kCGColorSpaceModelLab:
+ case kCGColorSpaceModelPattern:
+ case kCGColorSpaceModelUnknown:
+ {
+
+#ifdef RCT_DEBUG
+ //unsupported format
+ RCTLogError(@"Unsupported color model: %i", model);
+#endif
+
+ rgba[0] = 0.0;
+ rgba[1] = 0.0;
+ rgba[2] = 0.0;
+ rgba[3] = 1.0;
+ break;
+ }
+ }
+}
+
+NSString *RCTColorToHexString(CGColorRef color)
+{
+ CGFloat rgba[4];
+ RCTGetRGBAColorComponents(color, rgba);
+ uint8_t r = rgba[0]*255;
+ uint8_t g = rgba[1]*255;
+ uint8_t b = rgba[2]*255;
+ uint8_t a = rgba[3]*255;
+ if (a < 255) {
+ return [NSString stringWithFormat:@"#%02x%02x%02x%02x", r, g, b, a];
+ } else {
+ return [NSString stringWithFormat:@"#%02x%02x%02x", r, g, b];
+ }
+}
diff --git a/React/Views/RCTConvert+MapKit.m b/React/Views/RCTConvert+MapKit.m
index 9206c3a4050..dab5b766171 100644
--- a/React/Views/RCTConvert+MapKit.m
+++ b/React/Views/RCTConvert+MapKit.m
@@ -61,6 +61,11 @@ RCT_ENUM_CONVERTER(MKMapType, (@{
shape.hasLeftCallout = [RCTConvert BOOL:json[@"hasLeftCallout"]];
shape.hasRightCallout = [RCTConvert BOOL:json[@"hasRightCallout"]];
shape.animateDrop = [RCTConvert BOOL:json[@"animateDrop"]];
+ shape.tintColor = [RCTConvert UIColor:json[@"tintColor"]];
+ shape.image = [RCTConvert UIImage:json[@"image"]];
+ if (shape.tintColor && shape.image) {
+ shape.image = [shape.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ }
return shape;
}
diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m
index 5372793a816..75602578d33 100644
--- a/React/Views/RCTMapManager.m
+++ b/React/Views/RCTMapManager.m
@@ -14,6 +14,7 @@
#import "RCTConvert+MapKit.h"
#import "RCTEventDispatcher.h"
#import "RCTMap.h"
+#import "RCTUtils.h"
#import "UIView+React.h"
#import "RCTPointAnnotation.h"
@@ -55,6 +56,17 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
}
+- (NSDictionary *)constantsToExport
+{
+ return @{
+ @"PinColors": @{
+ @"RED": RCTColorToHexString([MKPinAnnotationView redPinColor].CGColor),
+ @"GREEN": RCTColorToHexString([MKPinAnnotationView greenPinColor].CGColor),
+ @"PURPLE": RCTColorToHexString([MKPinAnnotationView purplePinColor].CGColor),
+ }
+ };
+}
+
#pragma mark MKMapViewDelegate
- (void)mapView:(RCTMap *)mapView didSelectAnnotationView:(MKAnnotationView *)view
@@ -81,10 +93,41 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
return nil;
}
- MKPinAnnotationView *annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"RCTAnnotation"];
+ MKAnnotationView *annotationView;
+ if (annotation.image) {
+ if (annotation.tintColor) {
+ NSString *const reuseIdentifier = @"RCTImageViewAnnotation";
+ NSInteger imageViewTag = 99;
+ annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:reuseIdentifier];
+ if (!annotationView) {
+ annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
+ UIImageView *imageView = [UIImageView new];
+ imageView.tag = imageViewTag;
+ [annotationView addSubview:imageView];
+ }
+
+ UIImageView *imageView = (UIImageView *)[annotationView viewWithTag:imageViewTag];
+ imageView.image = annotation.image;
+ imageView.tintColor = annotation.tintColor;
+ [imageView sizeToFit];
+ imageView.center = CGPointZero;
+
+ } else {
+
+ NSString *reuseIdentifier = NSStringFromClass([MKAnnotationView class]);
+ annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:reuseIdentifier] ?: [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
+ annotationView.image = annotation.image;
+ }
+
+ } else {
+
+ NSString *reuseIdentifier = NSStringFromClass([MKPinAnnotationView class]);
+ annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:reuseIdentifier] ?: [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
+ ((MKPinAnnotationView *)annotationView).animatesDrop = annotation.animateDrop;
+ ((MKPinAnnotationView *)annotationView).pinTintColor = annotation.tintColor ?: [MKPinAnnotationView redPinColor];
+ }
annotationView.canShowCallout = true;
- annotationView.animatesDrop = annotation.animateDrop;
annotationView.leftCalloutAccessoryView = nil;
if (annotation.hasLeftCallout) {
@@ -172,13 +215,17 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
BOOL needZoom = NO;
CGFloat newLongitudeDelta = 0.0f;
MKCoordinateRegion region = mapView.region;
- // On iOS 7, it's possible that we observe invalid locations during initialization of the map.
- // Filter those out.
+
+ // On iOS 7, it's possible that we observe invalid locations during
+ // initialization of the map. Filter those out.
if (!CLLocationCoordinate2DIsValid(region.center)) {
return;
}
- // Calculation on float is not 100% accurate. If user zoom to max/min and then move, it's likely the map will auto zoom to max/min from time to time.
- // So let's try to make map zoom back to 99% max or 101% min so that there are some buffer that moving the map won't constantly hitting the max/min bound.
+
+ // Calculation on float is not 100% accurate. If user zoom to max/min and then
+ // move, it's likely the map will auto zoom to max/min from time to time.
+ // So let's try to make map zoom back to 99% max or 101% min so that there is
+ // some buffer, and moving the map won't constantly hit the max/min bound.
if (mapView.maxDelta > FLT_EPSILON && region.span.longitudeDelta > mapView.maxDelta) {
needZoom = YES;
newLongitudeDelta = mapView.maxDelta * (1 - RCTMapZoomBoundBuffer);
@@ -204,15 +251,13 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
return;
}
- #define FLUSH_NAN(value) (isnan(value) ? 0 : value)
-
mapView.onChange(@{
@"continuous": @(continuous),
@"region": @{
- @"latitude": @(FLUSH_NAN(region.center.latitude)),
- @"longitude": @(FLUSH_NAN(region.center.longitude)),
- @"latitudeDelta": @(FLUSH_NAN(region.span.latitudeDelta)),
- @"longitudeDelta": @(FLUSH_NAN(region.span.longitudeDelta)),
+ @"latitude": @(RCTZeroIfNaN(region.center.latitude)),
+ @"longitude": @(RCTZeroIfNaN(region.center.longitude)),
+ @"latitudeDelta": @(RCTZeroIfNaN(region.span.latitudeDelta)),
+ @"longitudeDelta": @(RCTZeroIfNaN(region.span.longitudeDelta)),
}
});
}
diff --git a/React/Views/RCTPointAnnotation.h b/React/Views/RCTPointAnnotation.h
index 0646608d480..1ee8fb05508 100644
--- a/React/Views/RCTPointAnnotation.h
+++ b/React/Views/RCTPointAnnotation.h
@@ -15,5 +15,7 @@
@property (nonatomic, assign) BOOL hasLeftCallout;
@property (nonatomic, assign) BOOL hasRightCallout;
@property (nonatomic, assign) BOOL animateDrop;
+@property (nonatomic, strong) UIColor *tintColor;
+@property (nonatomic, strong) UIImage *image;
@end