Skip to content

Commit

Permalink
[MapView] Support for annotation callouts, annotation press, callout …
Browse files Browse the repository at this point in the history
…presses and pin animation

Summary:
Started from here - facebook#1120. Most functionality for annotations were missing so I started implementing and somehow got caught up until the entire thing was done.

![screen shot 2015-05-12 at 10 07 43 pm](https://cloud.githubusercontent.com/assets/688326/7588677/8479a7a4-f8f9-11e4-99a4-1dc3c7691810.png)

2 new events:
- callout presses (left / right)
- annotation presses

6 new properties for annotations:
- hasLeftCallout
- hasRightCallout
- onLeftCalloutPress
- onRightCalloutPress
- animateDrop
- id

1 new property for MapView
- onAnnotationPress

---
Now the important thing is, that I implemented all of this the way "I would do it". I am not sure this is the 'reacty' way so please let me know my mistakes 😄

The problem is that there is no real way to identify annotations which makes it difficult to distinguish which one got clicked. The idea is to pass a `id` and whether it has callouts the entire way with the annotation. I had to
Closes facebook#1247
Github Author: David Mohl <me@dave.cx>

Test Plan: Imported from GitHub, without a `Test Plan:` line.
  • Loading branch information
dvcrn committed Jun 25, 2015
1 parent 3c5b4b0 commit 99bc08c
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 54 deletions.
90 changes: 86 additions & 4 deletions Libraries/Components/MapView/MapView.js
Expand Up @@ -35,6 +35,34 @@ type MapRegion = {
var MapView = React.createClass({ var MapView = React.createClass({
mixins: [NativeMethodsMixin], mixins: [NativeMethodsMixin],


checkAnnotationIds: function (annotations: Array<Object>) {

var newAnnotations = annotations.map(function (annotation) {
if (!annotation.id) {
// TODO: add a base64 (or similar) encoder here
annotation.id = encodeURIComponent(JSON.stringify(annotation));
}

return annotation;
});

this.setState({
annotations: newAnnotations
});
},

componentWillMount: function() {
if (this.props.annotations) {
this.checkAnnotationIds(this.props.annotations);
}
},

componentWillReceiveProps: function(nextProps: Object) {
if (nextProps.annotations) {
this.checkAnnotationIds(nextProps.annotations);
}
},

propTypes: { propTypes: {
/** /**
* Used to style and layout the `MapView`. See `StyleSheet.js` and * Used to style and layout the `MapView`. See `StyleSheet.js` and
Expand Down Expand Up @@ -84,14 +112,14 @@ var MapView = React.createClass({


/** /**
* The map type to be displayed. * The map type to be displayed.
* *
* - standard: standard road map (default) * - standard: standard road map (default)
* - satellite: satellite view * - satellite: satellite view
* - hybrid: satellite view with roads and points of interest overlayed * - hybrid: satellite view with roads and points of interest overlayed
*/ */
mapType: React.PropTypes.oneOf([ mapType: React.PropTypes.oneOf([
'standard', 'standard',
'satellite', 'satellite',
'hybrid', 'hybrid',
]), ]),


Expand Down Expand Up @@ -126,11 +154,34 @@ var MapView = React.createClass({
latitude: React.PropTypes.number.isRequired, latitude: React.PropTypes.number.isRequired,
longitude: React.PropTypes.number.isRequired, longitude: React.PropTypes.number.isRequired,


/**
* Whether the pin drop should be animated or not
*/
animateDrop: React.PropTypes.bool,

/** /**
* Annotation title/subtile. * Annotation title/subtile.
*/ */
title: React.PropTypes.string, title: React.PropTypes.string,
subtitle: React.PropTypes.string, subtitle: React.PropTypes.string,

/**
* Whether the Annotation has callout buttons.
*/
hasLeftCallout: React.PropTypes.bool,
hasRightCallout: React.PropTypes.bool,

/**
* Event handlers for callout buttons.
*/
onLeftCalloutPress: React.PropTypes.func,
onRightCalloutPress: React.PropTypes.func,

/**
* annotation id
*/
id: React.PropTypes.string

})), })),


/** /**
Expand Down Expand Up @@ -158,6 +209,11 @@ var MapView = React.createClass({
* Callback that is called once, when the user is done moving the map. * Callback that is called once, when the user is done moving the map.
*/ */
onRegionChangeComplete: React.PropTypes.func, onRegionChangeComplete: React.PropTypes.func,

/**
* Callback that is called once, when the user is clicked on a annotation.
*/
onAnnotationPress: React.PropTypes.func,
}, },


_onChange: function(event: Event) { _onChange: function(event: Event) {
Expand All @@ -170,8 +226,34 @@ var MapView = React.createClass({
} }
}, },


_onPress: function(event: Event) {
if (event.nativeEvent.action === 'annotation-click') {
this.props.onAnnotationPress && this.props.onAnnotationPress(event.nativeEvent.annotation);
}

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);
}
}
}

}
},

render: function() { render: function() {
return <RCTMap {...this.props} onChange={this._onChange} />; return <RCTMap {...this.props} onPress={this._onPress} onChange={this._onChange} />;
}, },
}); });


Expand Down
19 changes: 19 additions & 0 deletions React/Modules/RCTPointAnnotation.h
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import <MapKit/MapKit.h>

@interface RCTPointAnnotation : MKPointAnnotation <MKAnnotation>

@property (nonatomic, copy) NSString *identifier;
@property (nonatomic, assign) BOOL hasLeftCallout;
@property (nonatomic, assign) BOOL hasRightCallout;
@property (nonatomic, assign) BOOL animateDrop;

@end
14 changes: 14 additions & 0 deletions React/Modules/RCTPointAnnotation.m
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/

#import "RCTPointAnnotation.h"

@implementation RCTPointAnnotation

@end
6 changes: 6 additions & 0 deletions React/React.xcodeproj/project.pbxproj
Expand Up @@ -64,6 +64,7 @@
58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; };
58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; };
58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */; }; 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */; };
63F014C01B02080B003B75D2 /* RCTPointAnnotation.m in Sources */ = {isa = PBXBuildFile; fileRef = 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */; };
830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; };
830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; }; 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; };
832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; };
Expand Down Expand Up @@ -213,6 +214,8 @@
58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAsyncLocalStorage.h; sourceTree = "<group>"; }; 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAsyncLocalStorage.h; sourceTree = "<group>"; };
58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDatePickerManager.m; sourceTree = "<group>"; }; 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDatePickerManager.m; sourceTree = "<group>"; };
58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePickerManager.h; sourceTree = "<group>"; }; 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePickerManager.h; sourceTree = "<group>"; };
63F014BE1B02080B003B75D2 /* RCTPointAnnotation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointAnnotation.h; sourceTree = "<group>"; };
63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPointAnnotation.m; sourceTree = "<group>"; };
830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = "<group>"; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = "<group>"; };
830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = "<group>"; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = "<group>"; };
830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = "<group>"; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -291,6 +294,8 @@
13B07FEE1A69327A00A75B9A /* RCTTiming.m */, 13B07FEE1A69327A00A75B9A /* RCTTiming.m */,
13E067481A70F434002CDEE1 /* RCTUIManager.h */, 13E067481A70F434002CDEE1 /* RCTUIManager.h */,
13E067491A70F434002CDEE1 /* RCTUIManager.m */, 13E067491A70F434002CDEE1 /* RCTUIManager.m */,
63F014BE1B02080B003B75D2 /* RCTPointAnnotation.h */,
63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */,
); );
path = Modules; path = Modules;
sourceTree = "<group>"; sourceTree = "<group>";
Expand Down Expand Up @@ -599,6 +604,7 @@
830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */, 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */,
137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */,
00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */, 00C1A2B31AC0B7E000E89A1C /* RCTDevMenu.m in Sources */,
63F014C01B02080B003B75D2 /* RCTPointAnnotation.m in Sources */,
14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */,
134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */,
13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */,
Expand Down
15 changes: 8 additions & 7 deletions React/Views/RCTConvert+CoreLocation.h
@@ -1,10 +1,11 @@
// /**
// RCTConvert+CoreLocation.h * Copyright (c) 2015-present, Facebook, Inc.
// React * All rights reserved.
// *
// Created by Nick Lockwood on 12/04/2015. * This source code is licensed under the BSD-style license found in the
// Copyright (c) 2015 Facebook. All rights reserved. * LICENSE file in the root directory of this source tree. An additional grant
// * of patent rights can be found in the PATENTS file in the same directory.
*/


#import <CoreLocation/CoreLocation.h> #import <CoreLocation/CoreLocation.h>


Expand Down
15 changes: 8 additions & 7 deletions React/Views/RCTConvert+CoreLocation.m
@@ -1,10 +1,11 @@
// /**
// RCTConvert+CoreLocation.m * Copyright (c) 2015-present, Facebook, Inc.
// React * All rights reserved.
// *
// Created by Nick Lockwood on 12/04/2015. * This source code is licensed under the BSD-style license found in the
// Copyright (c) 2015 Facebook. All rights reserved. * LICENSE file in the root directory of this source tree. An additional grant
// * of patent rights can be found in the PATENTS file in the same directory.
*/


#import "RCTConvert+CoreLocation.h" #import "RCTConvert+CoreLocation.h"


Expand Down
20 changes: 13 additions & 7 deletions React/Views/RCTConvert+MapKit.h
@@ -1,13 +1,15 @@
// /**
// RCTConvert+MapKit.h * Copyright (c) 2015-present, Facebook, Inc.
// React * All rights reserved.
// *
// Created by Nick Lockwood on 12/04/2015. * This source code is licensed under the BSD-style license found in the
// Copyright (c) 2015 Facebook. All rights reserved. * LICENSE file in the root directory of this source tree. An additional grant
// * of patent rights can be found in the PATENTS file in the same directory.
*/


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


#import "RCTPointAnnotation.h"
#import "RCTConvert.h" #import "RCTConvert.h"


@interface RCTConvert (MapKit) @interface RCTConvert (MapKit)
Expand All @@ -16,8 +18,12 @@
+ (MKCoordinateRegion)MKCoordinateRegion:(id)json; + (MKCoordinateRegion)MKCoordinateRegion:(id)json;
+ (MKShape *)MKShape:(id)json; + (MKShape *)MKShape:(id)json;
+ (MKMapType)MKMapType:(id)json; + (MKMapType)MKMapType:(id)json;
+ (RCTPointAnnotation *)RCTPointAnnotation:(id)json;


typedef NSArray MKShapeArray; typedef NSArray MKShapeArray;
+ (MKShapeArray *)MKShapeArray:(id)json; + (MKShapeArray *)MKShapeArray:(id)json;


typedef NSArray RCTPointAnnotationArray;
+ (RCTPointAnnotationArray *)RCTPointAnnotationArray:(id)json;

@end @end
33 changes: 25 additions & 8 deletions React/Views/RCTConvert+MapKit.m
@@ -1,14 +1,15 @@
// /**
// RCTConvert+MapKit.m * Copyright (c) 2015-present, Facebook, Inc.
// React * All rights reserved.
// *
// Created by Nick Lockwood on 12/04/2015. * This source code is licensed under the BSD-style license found in the
// Copyright (c) 2015 Facebook. All rights reserved. * LICENSE file in the root directory of this source tree. An additional grant
// * of patent rights can be found in the PATENTS file in the same directory.
*/


#import "RCTConvert+MapKit.h" #import "RCTConvert+MapKit.h"

#import "RCTConvert+CoreLocation.h" #import "RCTConvert+CoreLocation.h"
#import "RCTPointAnnotation.h"


@implementation RCTConvert(MapKit) @implementation RCTConvert(MapKit)


Expand Down Expand Up @@ -49,4 +50,20 @@ + (MKShape *)MKShape:(id)json
@"hybrid": @(MKMapTypeHybrid), @"hybrid": @(MKMapTypeHybrid),
}), MKMapTypeStandard, integerValue) }), MKMapTypeStandard, integerValue)


+ (RCTPointAnnotation *)RCTPointAnnotation:(id)json
{
json = [self NSDictionary:json];
RCTPointAnnotation *shape = [[RCTPointAnnotation alloc] init];
shape.coordinate = [self CLLocationCoordinate2D:json];
shape.title = [RCTConvert NSString:json[@"title"]];
shape.subtitle = [RCTConvert NSString:json[@"subtitle"]];
shape.identifier = [RCTConvert NSString:json[@"id"]];
shape.hasLeftCallout = [RCTConvert BOOL:json[@"hasLeftCallout"]];
shape.hasRightCallout = [RCTConvert BOOL:json[@"hasRightCallout"]];
shape.animateDrop = [RCTConvert BOOL:json[@"animateDrop"]];
return shape;
}

RCT_ARRAY_CONVERTER(RCTPointAnnotation)

@end @end
3 changes: 2 additions & 1 deletion React/Views/RCTMap.h
Expand Up @@ -26,7 +26,8 @@ extern const CGFloat RCTMapZoomBoundBuffer;
@property (nonatomic, assign) CGFloat maxDelta; @property (nonatomic, assign) CGFloat maxDelta;
@property (nonatomic, assign) UIEdgeInsets legalLabelInsets; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets;
@property (nonatomic, strong) NSTimer *regionChangeObserveTimer; @property (nonatomic, strong) NSTimer *regionChangeObserveTimer;
@property (nonatomic, strong) NSMutableArray *annotationIds;


- (void)setAnnotations:(MKShapeArray *)annotations; - (void)setAnnotations:(RCTPointAnnotationArray *)annotations;


@end @end
48 changes: 44 additions & 4 deletions React/Views/RCTMap.m
Expand Up @@ -112,12 +112,52 @@ - (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
[super setRegion:region animated:animated]; [super setRegion:region animated:animated];
} }


- (void)setAnnotations:(MKShapeArray *)annotations - (void)setAnnotations:(RCTPointAnnotationArray *)annotations
{ {
[self removeAnnotations:self.annotations]; NSMutableArray *newAnnotationIds = [[NSMutableArray alloc] init];
if (annotations.count) { NSMutableArray *annotationsToDelete = [[NSMutableArray alloc] init];
[self addAnnotations:annotations]; NSMutableArray *annotationsToAdd = [[NSMutableArray alloc] init];

for (RCTPointAnnotation *annotation in annotations) {
if (![annotation isKindOfClass:[RCTPointAnnotation class]]) {
continue;
}

[newAnnotationIds addObject:annotation.identifier];

// If the current set does not contain the new annotation, mark it as add
if (![self.annotationIds containsObject:annotation.identifier]) {
[annotationsToAdd addObject:annotation];
}
}

for (RCTPointAnnotation *annotation in self.annotations) {
if (![annotation isKindOfClass:[RCTPointAnnotation class]]) {
continue;
}

// If the new set does not contain an existing annotation, mark it as delete
if (![newAnnotationIds containsObject:annotation.identifier]) {
[annotationsToDelete addObject:annotation];
}
}

if (annotationsToDelete.count) {
[self removeAnnotations:annotationsToDelete];
}

if (annotationsToAdd.count) {
[self addAnnotations:annotationsToAdd];
}

NSMutableArray *newIds = [[NSMutableArray alloc] init];
for (RCTPointAnnotation *anno in self.annotations) {
if ([anno isKindOfClass:[MKUserLocation class]]) {
continue;
}
[newIds addObject:anno.identifier];
} }
self.annotationIds = newIds;
} }


@end @end

0 comments on commit 99bc08c

Please sign in to comment.