View annotations

Add view annotations on top of the map view using the Mapbox Maps SDK's View Annotations API. View annotations are Android Views that are drawn on top of a Mapbox MapView and bound to some Geometry. This can be helpful for achieving common pattern used in maps in mobile applications like displaying info window when POI (point of interest) is tapped.

Supported geometry types

Only Point geometry is supported for now. Supporting other geometry types will be added in upcoming releases.

Benefits:

  • Straight-forward API that allows adding an Android View as a view annotation, making possible to add clickable buttons or any other Android UI elements.
  • High rendering performance when using reasonable number of views (generally below 100, but may vary based on the view's content and the user's device model).
  • Wide number of options including allowing overlap between view annotations, selected state, connecting to a map feature, and more.

Limitations:

  • Inefficient and poor performance when adding many features (> 250) to the map if allow overlap is turned on.
  • Low-level API that requires adding code on the user's end for advanced cases.

Create a view annotation

To create a view annotation a few things are required:

  • An Android view represented as either an XML layout that will be inflated by the view annotation manager or a View prepared beforehand.
  • A ViewAnnotationManager instance.

Create a layout

Start by creating a new layout that contains the contents of the view annotation. This can include anything from text to images to interactive elements and more.

For example, to create a minimal layout containing text:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="60dp"
    android:background="@android:color/white"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/annotation"
        android:text="hello world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textSize="20sp"
        android:padding="3dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
Limitations when inflating from XML

Passing android:layout_width="wrap_content" or android:layout_height="wrap_content" as dimensions for the root layout is not supported, but using those dimensions for nested elements will work as expected. Supporting wrap content dimensions will be added in upcoming releases.

Add a view annotation to the MapView

Start by getting a ViewAnnotationManager instance:

val viewAnnotationManager = binding.mapView.viewAnnotationManager

Then use the manager's addViewAnnotation method to create the view annotation. Three overloaded methods are available:

  1. Taking a View that is already inflated.
  2. Taking an XML identifier resId and returning a synchronously inflated View.
  3. Taking an XML identifier resId and returning an inflated View in asyncInflateCallback. If using this method, you must add async inflater dependency to your project explicitly (any 1.x.x version should work).

All methods above also require ViewAnnotationOptions with at least geometry field specified.

Then to add the view annotation on top of the MapView, start by:

  1. Creating a new view annotation.
  2. Setting resId to the layout created above.
  3. Defining options for the view annotation.
private fun addViewAnnotation(point: Point) {
    // Define the view annotation
    val viewAnnotation = viewAnnotationManager.addViewAnnotation(
        // Specify the layout resource id
        resId = R.layout.annotation_view,
        // Set any view annotation options
        options = viewAnnotationOptions {
            geometry(point)
        }
    )
    AnnotationViewBinding.bind(viewAnnotation)
}

Add the view annotation on view load:

private lateinit var mapboxMap: MapboxMap
private lateinit var viewAnnotationManager: ViewAnnotationManager

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    // Create view annotation manager
    viewAnnotationManager = binding.mapView.viewAnnotationManager
    mapboxMap = binding.mapView.getMapboxMap().apply {
        // Load a map style
        loadStyleUri(Style.MAPBOX_STREETS) {
            // Get the center point of the map
            val center = mapboxMap.cameraState.center
            // Add the view annotation at the center point
            addViewAnnotation(center)
        }
    }
}
More view annotation options

There are many more options available when adding or updating the view annotation. See the complete list of options in theViewAnnotationOptions documentation.

Customize appearance

Specify order

The z-index of view annotations is based on the order in which they are added. This allows you to control which view is visible when multiple views intersect on the screen.

To bring a view annotation on top of others regardless of the order in which it was added, use the selected property:

viewAnnotationManager.updateViewAnnotation(
  existingViewAnnotation,
  viewAnnotationOptions {
    selected(true)
  }
)

Any number of view annotations could be in selected state. The z-index among selected view annotations also respects the order in which they are added.

Default order
View annotations with z-index based on the order in which they were added: Hello world!, Прывітанне Сусвет!, Hallo Welt!, Hei maailma!
Override order
Use the selected property to bring the Hello world! annotation on top of others regardless of the order in which they were added.

View annotations do not rotate, pitch, or scale with the map, but some properties (including width, height, and additional offset in pixels) can be controlled by the user using update operation.

viewAnnotationManager.updateViewAnnotation(
  existingViewAnnotation,
  viewAnnotationOptions {
    width(newWidth)
    height(newHeight)
    // move view annotation to the right on 10 pixels
    offsetX(10)
    // move view annotation to the bottom on 20 pixels
    offsetY(-20)
  }
)

Handle visibility

When a view annotation is added or updated, visibility can be specified explicitly.

viewAnnotationManager.addViewAnnotation(
  R.layout.some_layout,
  viewAnnotationOptions {
    geometry(point)
    // Set view annotation to be not visible on the map
    // even if actual view is [View.VISIBLE]
    visible(false)
  }
)

However for most use cases there is no need to specify visibility explicitly as view annotation manager controls actual view positioning and visibility automatically. This means that everything will work as expected if you only work with the view's visibility option.

val view = viewAnnotationManager.addViewAnnotation(
  // Assume inflated view is visible by default
  R.layout.some_layout,
  viewAnnotationOptions {
    // Do not setting visibility explicitly
    geometry(point)
  }
)
// Hide actual view - it will be properly hidden both from the screen 
// as well as from positioning calculations happening in the SDK
view.visibility = View.GONE
// No need to call viewAnnotationManager.updateViewAnnotation(view, viewAnnotationOptions { visible = false })

Common uses

The flexibility of view annotations means that there are countless ways to use this feature, but there are a few common uses outlined below to help you get started.

Display on map click

You can add view annotations when a user interacts with the map.

When the user clicks on the map, you can get information from the map camera, position (like in the example linked below), or data in the map to determine the contents of the view annotation using one of MapboxMap's map feature query methods.

example
View annotations: basic example

Add view annotation to the map

Attach to a style layer feature

View annotations are not explicitly bound to any style sources, but ViewAnnotationOptions.associatedFeatureId could be used to bind given view annotation with some Feature by Feature.id.

The visibility of the feature will then determine the visibility of the view annotation. For example, if the icon is not visible because it will collide with some other icon, the associated view annotation will also disappear from the screen.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  val mapView = MapView(this)
  setContentView(mapView)

  val bitmap = BitmapFactory.decodeResource(resources, R.drawable.marker_image)
  markerHeight = bitmap.height
  
  // unique id we will add both to source and to view annotation
  val markerId = "unique_id"
  val viewAnnotationManager = mapView.viewAnnotationManager
  
  mapboxMap = binding.mapView.getMapboxMap().apply {
    val center = this.cameraState.center
    loadStyle(
      // add style with empty geojson for now and symbol layer representing marker icon
      styleExtension = prepareStyle(Style.MAPBOX_STREETS, bitmap, markerId, center)
    ) {
      viewAnnotationManager.addViewAnnotation(
        resId = R.layout.view_annotation_layout,
        options = viewAnnotationOptions {
          geometry(center)
          // specify associatedFeatureId same as the one we've specified for the feature
          associatedFeatureId(markerId)
          // synchronize anchor with symbol layer anchor
          anchor(ViewAnnotationAnchor.BOTTOM)
          // manually specify offsetY so that view annotation will appear above the marker icon
          // otherwise they will overlap as both view annotation and symbol layer are tied to same POINT
          offsetY(markerHeight)
        }
      )
    }
  }
}

private fun prepareStyle(styleUri: String, bitmap: Bitmap, associatedFeatureId: String, center: Point) = style(styleUri) {
    +image("marker_image") {
        bitmap(bitmap)
    }
    +geoJsonSource("new_source_id") {
        feature(Feature.fromGeometry(center, null, markerId))
    }
    +symbolLayer("new_layer_id", "new_source_id") {
        iconImage("marker_image")
        iconAnchor(IconAnchor.BOTTOM)
        iconAllowOverlap(false)
    }
}
Note

While the View Annotation API does not come with built-in logic for handling opening and closing a callout attached to a symbol layer feature when the feature is click, you can see one possible way to implement that functionality in the View annotations: advanced example.

example
View annotations: advanced example

Add view annotation anchored to a symbol layer feature