Added ability to use a custom view for MapView annotations

Summary:
public
This diff adds the ability to specify a custom React component (aka view) to be displayed as a MapView pin.

This makes it possible to use remote images (using an <Image/> component), or text (using a <Text/> component), or anything else.

One consequence of this is that MapView can no longer support arbitrary subviews. To place views in front the map, add them to a separate container view.

Reviewed By: tadeuzagallo

Differential Revision: D2764790

fb-gh-sync-id: e16b44e866c2d76c76b0cb35ef9eefbfc68d6719
This commit is contained in:
Nick Lockwood
2015-12-17 06:45:53 -08:00
committed by facebook-github-bot-6
parent 97c75cf5a4
commit f9dfb90a35
8 changed files with 339 additions and 297 deletions

View File

@@ -23,10 +23,7 @@
+ (RCTMapAnnotation *)RCTMapAnnotation:(id)json;
+ (RCTMapOverlay *)RCTMapOverlay:(id)json;
typedef NSArray RCTMapAnnotationArray;
+ (NSArray<RCTMapAnnotation *> *)RCTMapAnnotationArray:(id)json;
typedef NSArray RCTMapOverlayArray;
+ (NSArray<RCTMapOverlay *> *)RCTMapOverlayArray:(id)json;
@end

View File

@@ -50,9 +50,14 @@ RCT_ENUM_CONVERTER(MKMapType, (@{
annotation.animateDrop = [RCTConvert BOOL:json[@"animateDrop"]];
annotation.tintColor = [RCTConvert UIColor:json[@"tintColor"]];
annotation.image = [RCTConvert UIImage:json[@"image"]];
if (annotation.tintColor && annotation.image) {
annotation.image = [annotation.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
annotation.viewIndex =
[RCTConvert NSInteger:json[@"viewIndex"] ?: @(NSNotFound)];
annotation.leftCalloutViewIndex =
[RCTConvert NSInteger:json[@"leftCalloutViewIndex"] ?: @(NSNotFound)];
annotation.rightCalloutViewIndex =
[RCTConvert NSInteger:json[@"rightCalloutViewIndex"] ?: @(NSNotFound)];
annotation.detailCalloutViewIndex =
[RCTConvert NSInteger:json[@"detailCalloutViewIndex"] ?: @(NSNotFound)];
return annotation;
}

View File

@@ -31,7 +31,7 @@ RCT_EXTERN const CGFloat RCTMapZoomBoundBuffer;
@property (nonatomic, copy) RCTBubblingEventBlock onChange;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
- (void)setAnnotations:(RCTMapAnnotationArray *)annotations;
- (void)setOverlays:(RCTMapOverlayArray *)overlays;
- (void)setAnnotations:(NSArray<RCTMapAnnotation *> *)annotations;
- (void)setOverlays:(NSArray<RCTMapOverlay *> *)overlays;
@end

View File

@@ -23,6 +23,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
{
UIView *_legalLabel;
CLLocationManager *_locationManager;
NSMutableArray<UIView *> *_reactSubviews;
}
- (instancetype)init
@@ -30,6 +31,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
if ((self = [super init])) {
_hasStartedRendering = NO;
_reactSubviews = [NSMutableArray new];
// Find Apple link label
for (UIView *subview in self.subviews) {
@@ -49,6 +51,21 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
[_regionChangeObserveTimer invalidate];
}
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
{
[_reactSubviews insertObject:subview atIndex:atIndex];
}
- (void)removeReactSubview:(UIView *)subview
{
[_reactSubviews removeObject:subview];
}
- (NSArray<UIView *> *)reactSubviews
{
return _reactSubviews;
}
- (void)layoutSubviews
{
[super layoutSubviews];
@@ -111,7 +128,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
// TODO: this doesn't preserve order. Should it? If so we should change the
// algorithm. If not, it would be more efficient to use an NSSet
- (void)setAnnotations:(RCTMapAnnotationArray *)annotations
- (void)setAnnotations:(NSArray<RCTMapAnnotation *> *)annotations
{
NSMutableArray<NSString *> *newAnnotationIDs = [NSMutableArray new];
NSMutableArray<RCTMapAnnotation *> *annotationsToDelete = [NSMutableArray new];
@@ -154,7 +171,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
// TODO: this doesn't preserve order. Should it? If so we should change the
// algorithm. If not, it would be more efficient to use an NSSet
- (void)setOverlays:(RCTMapOverlayArray *)overlays
- (void)setOverlays:(NSArray<RCTMapOverlay *> *)overlays
{
NSMutableArray *newOverlayIDs = [NSMutableArray new];
NSMutableArray *overlaysToDelete = [NSMutableArray new];

View File

@@ -17,5 +17,9 @@
@property (nonatomic, assign) BOOL animateDrop;
@property (nonatomic, strong) UIColor *tintColor;
@property (nonatomic, strong) UIImage *image;
@property (nonatomic, assign) NSInteger viewIndex;
@property (nonatomic, assign) NSInteger leftCalloutViewIndex;
@property (nonatomic, assign) NSInteger rightCalloutViewIndex;
@property (nonatomic, assign) NSInteger detailCalloutViewIndex;
@end

View File

@@ -67,8 +67,8 @@ RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType)
RCT_EXPORT_VIEW_PROPERTY(annotations, RCTMapAnnotationArray)
RCT_EXPORT_VIEW_PROPERTY(overlays, RCTMapOverlayArray)
RCT_EXPORT_VIEW_PROPERTY(annotations, NSArray<RCTMapAnnotation *>)
RCT_EXPORT_VIEW_PROPERTY(overlays, NSArray<RCTMapOverlay *>)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
@@ -125,77 +125,124 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
}
}
- (MKAnnotationView *)mapView:(__unused MKMapView *)mapView viewForAnnotation:(RCTMapAnnotation *)annotation
- (MKAnnotationView *)mapView:(RCTMap *)mapView
viewForAnnotation:(RCTMapAnnotation *)annotation
{
if (![annotation isKindOfClass:[RCTMapAnnotation class]]) {
return nil;
}
MKAnnotationView *annotationView;
if (annotation.image) {
if (annotation.tintColor) {
annotationView.clipsToBounds = YES;
if (annotation.viewIndex != NSNotFound) {
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;
NSString *const reuseIdentifier = @"RCTCustomViewAnnotation";
annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:reuseIdentifier];
if (!annotationView) {
annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:reuseIdentifier];
}
for (UIView *view in annotationView.subviews) {
[view removeFromSuperview];
}
UIView *reactView = mapView.reactSubviews[annotation.viewIndex];
annotationView.bounds = reactView.frame;
[annotationView addSubview:reactView];
} else if (annotation.image) {
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];
annotationView =
[mapView dequeueReusableAnnotationViewWithIdentifier:reuseIdentifier] ?:
[[MKPinAnnotationView alloc] initWithAnnotation:annotation
reuseIdentifier:reuseIdentifier];
((MKPinAnnotationView *)annotationView).animatesDrop = annotation.animateDrop;
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0
if (![annotationView respondsToSelector:@selector(pinTintColor)]) {
NSString *hexColor = annotation.tintColor ? RCTColorToHexString(annotation.tintColor.CGColor) : RCTMapPinRed;
((MKPinAnnotationView *)annotationView).pinColor = [RCTConvert MKPinAnnotationColor:hexColor];
NSString *hexColor = annotation.tintColor ?
RCTColorToHexString(annotation.tintColor.CGColor) : RCTMapPinRed;
((MKPinAnnotationView *)annotationView).pinColor =
[RCTConvert MKPinAnnotationColor:hexColor];
} else
#endif
{
((MKPinAnnotationView *)annotationView).pinTintColor = annotation.tintColor ?: [MKPinAnnotationView redPinColor];
((MKPinAnnotationView *)annotationView).pinTintColor =
annotation.tintColor ?: [MKPinAnnotationView redPinColor];
}
}
annotationView.canShowCallout = true;
annotationView.leftCalloutAccessoryView = nil;
if (annotation.hasLeftCallout) {
annotationView.leftCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
if (annotation.leftCalloutViewIndex != NSNotFound) {
annotationView.leftCalloutAccessoryView =
mapView.reactSubviews[annotation.leftCalloutViewIndex];
} else if (annotation.hasLeftCallout) {
annotationView.leftCalloutAccessoryView =
[UIButton buttonWithType:UIButtonTypeDetailDisclosure];
} else {
annotationView.leftCalloutAccessoryView = nil;
}
annotationView.rightCalloutAccessoryView = nil;
if (annotation.hasRightCallout) {
annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
if (annotation.rightCalloutViewIndex != NSNotFound) {
annotationView.rightCalloutAccessoryView =
mapView.reactSubviews[annotation.rightCalloutViewIndex];
} else if (annotation.hasRightCallout) {
annotationView.rightCalloutAccessoryView =
[UIButton buttonWithType:UIButtonTypeDetailDisclosure];
} else {
annotationView.rightCalloutAccessoryView = nil;
}
//http://stackoverflow.com/questions/32581049/mapkit-ios-9-detailcalloutaccessoryview-usage
if ([annotationView respondsToSelector:@selector(detailCalloutAccessoryView)]) {
if (annotation.detailCalloutViewIndex != NSNotFound) {
UIView *calloutView = mapView.reactSubviews[annotation.detailCalloutViewIndex];
NSLayoutConstraint *widthConstraint =
[NSLayoutConstraint constraintWithItem:calloutView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:calloutView.frame.size.width];
[calloutView addConstraint:widthConstraint];
NSLayoutConstraint *heightConstraint =
[NSLayoutConstraint constraintWithItem:calloutView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:calloutView.frame.size.height];
[calloutView addConstraint:heightConstraint];
annotationView.detailCalloutAccessoryView = calloutView;
} else {
annotationView.detailCalloutAccessoryView = nil;
}
}
return annotationView;
}
- (MKOverlayRenderer *)mapView:(__unused MKMapView *)mapView rendererForOverlay:(RCTMapOverlay *)overlay
- (MKOverlayRenderer *)mapView:(__unused MKMapView *)mapView
rendererForOverlay:(RCTMapOverlay *)overlay
{
if ([overlay isKindOfClass:[RCTMapOverlay class]]) {
MKPolylineRenderer *polylineRenderer = [[MKPolylineRenderer alloc] initWithPolyline:overlay];
MKPolylineRenderer *polylineRenderer =
[[MKPolylineRenderer alloc] initWithPolyline:overlay];
polylineRenderer.strokeColor = overlay.strokeColor;
polylineRenderer.lineWidth = overlay.lineWidth;
return polylineRenderer;
@@ -204,11 +251,12 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
}
}
- (void)mapView:(RCTMap *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
- (void)mapView:(RCTMap *)mapView annotationView:(MKAnnotationView *)view
calloutAccessoryControlTapped:(UIControl *)control
{
if (mapView.onPress) {
// Pass to js
// Pass to JS
RCTMapAnnotation *annotation = (RCTMapAnnotation *)view.annotation;
mapView.onPress(@{
@"side": (control == view.leftCalloutAccessoryView) ? @"left" : @"right",
@@ -236,13 +284,15 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
{
[self _regionChanged:mapView];
mapView.regionChangeObserveTimer = [NSTimer timerWithTimeInterval:RCTMapRegionChangeObserveInterval
target:self
selector:@selector(_onTick:)
userInfo:@{ RCTMapViewKey: mapView }
repeats:YES];
mapView.regionChangeObserveTimer =
[NSTimer timerWithTimeInterval:RCTMapRegionChangeObserveInterval
target:self
selector:@selector(_onTick:)
userInfo:@{ RCTMapViewKey: mapView }
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:mapView.regionChangeObserveTimer forMode:NSRunLoopCommonModes];
[[NSRunLoop mainRunLoop] addTimer:mapView.regionChangeObserveTimer
forMode:NSRunLoopCommonModes];
}
- (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(__unused BOOL)animated
@@ -288,15 +338,18 @@ RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
// 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) {
if (mapView.maxDelta > FLT_EPSILON &&
region.span.longitudeDelta > mapView.maxDelta) {
needZoom = YES;
newLongitudeDelta = mapView.maxDelta * (1 - RCTMapZoomBoundBuffer);
} else if (mapView.minDelta > FLT_EPSILON && region.span.longitudeDelta < mapView.minDelta) {
} else if (mapView.minDelta > FLT_EPSILON &&
region.span.longitudeDelta < mapView.minDelta) {
needZoom = YES;
newLongitudeDelta = mapView.minDelta * (1 + RCTMapZoomBoundBuffer);
}
if (needZoom) {
region.span.latitudeDelta = region.span.latitudeDelta / region.span.longitudeDelta * newLongitudeDelta;
region.span.latitudeDelta =
region.span.latitudeDelta / region.span.longitudeDelta * newLongitudeDelta;
region.span.longitudeDelta = newLongitudeDelta;
mapView.region = region;
}