Posted at

Android Custom Info Window for GoogleMaps Marker responsive to touch events

When the user clicks on a marker, an info window is expected. Here, we demonstrate how to add a custom info window (a subclass of UIView) which responds to user touch events.

First, we must create the CustomCalloutView, a container which can hold any content. Regardless of the contents, we must add a tap gesture recognizer so that this callout can respond to touch events.

class CustomInfoWindowView: UIView {

convenience init() {
let tap = UITapGestureRecognizer(target: self, action: #selector(onTap))
tap.numberOfTapsRequired = 1
self.addGestureRecognizer(tap)
}

@objc func onTap() {}
}

We must also create a structure that links a marker with its associated custom info window.

When a custom info window is showing, we should maintain a reference to this structure so that future updates to its position can be performed.

struct SelectedInfoWindow {

var marker: GMSMarker? = nil
var infoWindow: UIView? = nil

init(infoWindow: UIView, marker: GMSMarker) {
self.infoWindow = infoWindow
self.marker = marker
}
}

When the user taps a marker, we add the CustomCalloutView as a subview of the mapView, making sure to generate the reference structure above after removing past info windows.

We also immediately set the position of the info window by calling self.updateInfoWindowPosition() as described below. This function must return "true" to ensure that the default info window does not appear.

func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {

self.removeInfoWindow()
self.mapView?.subviews.filter{ $0 is CustomInfoWindowView }.forEach{
$0.removeFromSuperview()
}

let infoWindow = CustomInfoWindowView()
self.selectedInfoWindow = SelectedInfoWindow(infoWindow: infoWindow, marker: marker)
self.mapView?.addSubview(infoWindow)
self.updateInfoWindowPosition()

return true
}

Here, an info window is removed and the reference structure deleted.

func removeInfoWindow() {

guard
let window = self.selectedInfoWindow?.infoWindow,
let marker = self.selectedInfoWindow?.marker
else {
return
}

infoWindow.removeFromSuperview()
self.selectedInfoWindow = nil
}

The central position of the info window is set relative to the center point of its associated marker, using mapView?.projection.point(for:). It is recommended to offset these centers so that the marker is not covered by the info window; this offset is therefore calculated from the size of the marker icon and custom infoWindow.

We should also call this function if the CustomCalloutView layout changes (i.e. override layoutSubViews to catch this event), because the infoWindow offset may have changed.

public func updateInfoWindowPosition() {

guard
let infoWindow = self.selectedInfoWindow?.infoWindow,
let marker = self.selectedInfoWindow?.marker,
let center = self.mapView?.projection.point(for: marker.position)
else { return }

window.center = center

let markerHeight = marker.icon?.size.height ?? 0.0
let infoWindowHeight = infoWindow.frame.height
var offset: CGFloat = -0.5 * infoWindowHeight - markerHeight

infoWindow.frame = window.frame.offsetBy(dx: 0.0, dy: offset)
}

When the map position changes, we must continue to update the infoWindow position.

func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) {

self.updateInfoWindowPosition()
}

Finally, when the user deselects the marker by pressing the map at a location other than a marker, we must call the method to remove the info window.

func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) {

self.removeInfoWindow()
}