Skip to main content

Place Autocomplete + Search UI + Maps SDK

Note

This example is a part of the Search SDK for Android sample app. You can find the values for all referenced resources in the res directory. For example, see res/values/strings.xml for R.string.* references used in this example.

Besides Place Autocomplete and Search UI this example also uses utility class SampleAppUtils.kt and Mapbox Maps SDK. You need to add it as a dependency using the Maps SDK v10 installation instructions.

activity_place_autocomplete
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/windowBackground"
tools:context=".AddressAutofillUiActivity"
>

<EditText
android:id="@+id/query_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:autofillHints="@null"
android:background="@drawable/card_background"
android:elevation="4dp"
android:hint="@string/place_autocomplete_query_hint"
android:inputType="text"
android:minHeight="?actionBarSize"
android:paddingHorizontal="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>

<com.mapbox.maps.MapView
android:id="@+id/map_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:mapbox_logoGravity="bottom"
/>

<com.mapbox.search.ui.view.SearchResultsView
android:id="@+id/search_results_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:background="@drawable/card_background"
android:clipToPadding="false"
android:elevation="4dp"
android:paddingTop="8dp"
android:paddingBottom="22dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/query_text"
/>

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/search_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="@dimen/search_card_elevation"
>

<com.mapbox.search.ui.view.place.SearchPlaceBottomSheetView
android:id="@+id/search_place_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:elevation="@dimen/search_card_elevation"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
PlaceAutocompleteUiActivity.kt
package com.mapbox.search.sample

import android.Manifest
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.mapbox.android.core.location.LocationEngineProvider
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.EdgeInsets
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
import com.mapbox.maps.plugin.annotation.annotations
import com.mapbox.maps.plugin.annotation.generated.CircleAnnotationOptions
import com.mapbox.maps.plugin.annotation.generated.createCircleAnnotationManager
import com.mapbox.maps.plugin.gestures.addOnMapLongClickListener
import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.search.autocomplete.PlaceAutocomplete
import com.mapbox.search.autocomplete.PlaceAutocompleteOptions
import com.mapbox.search.autocomplete.PlaceAutocompleteSuggestion
import com.mapbox.search.autocomplete.PlaceAutocompleteType
import com.mapbox.search.ui.adapter.autocomplete.PlaceAutocompleteUiAdapter
import com.mapbox.search.ui.view.CommonSearchViewConfiguration
import com.mapbox.search.ui.view.SearchResultsView
import com.mapbox.search.ui.view.place.SearchPlace
import com.mapbox.search.ui.view.place.SearchPlaceBottomSheetView

class PlaceAutocompleteUiActivity : AppCompatActivity() {

private lateinit var placeAutocomplete: PlaceAutocomplete

private lateinit var searchResultsView: SearchResultsView
private lateinit var placeAutocompleteUiAdapter: PlaceAutocompleteUiAdapter

private lateinit var queryEditText: EditText

private lateinit var mapView: MapView
private lateinit var mapboxMap: MapboxMap
private lateinit var mapMarkersManager: MapMarkersManager

private lateinit var searchPlaceView: SearchPlaceBottomSheetView

private var ignoreNextQueryUpdate = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_place_autocomplete)

placeAutocomplete = PlaceAutocomplete.create(getString(R.string.mapbox_access_token))

queryEditText = findViewById(R.id.query_text)

mapView = findViewById(R.id.map_view)
mapView.getMapboxMap().also { mapboxMap ->
this.mapboxMap = mapboxMap

mapboxMap.loadStyleUri(Style.MAPBOX_STREETS) {
mapView.location.updateSettings {
enabled = true
}

mapView.location.addOnIndicatorPositionChangedListener(object : OnIndicatorPositionChangedListener {
override fun onIndicatorPositionChanged(point: Point) {
mapView.getMapboxMap().setCamera(
CameraOptions.Builder()
.center(point)
.zoom(14.0)
.build()
)

mapView.location.removeOnIndicatorPositionChangedListener(this)
}
})
}
}

mapMarkersManager = MapMarkersManager(mapView)

mapboxMap.addOnMapLongClickListener {
reverseGeocoding(it)
return@addOnMapLongClickListener true
}

searchResultsView = findViewById(R.id.search_results_view)

searchResultsView.initialize(
SearchResultsView.Configuration(
commonConfiguration = CommonSearchViewConfiguration()
)
)

placeAutocompleteUiAdapter = PlaceAutocompleteUiAdapter(
view = searchResultsView,
placeAutocomplete = placeAutocomplete
)

searchPlaceView = findViewById<SearchPlaceBottomSheetView>(R.id.search_place_view).apply {
initialize(CommonSearchViewConfiguration())

isFavoriteButtonVisible = false

addOnCloseClickListener {
hide()
closePlaceCard()
}

addOnNavigateClickListener { searchPlace ->
startActivity(geoIntent(searchPlace.coordinate))
}

addOnShareClickListener { searchPlace ->
startActivity(shareIntent(searchPlace))
}
}

LocationEngineProvider.getBestLocationEngine(applicationContext).lastKnownLocation(this) { point ->
point?.let {
mapView.getMapboxMap().setCamera(
CameraOptions.Builder()
.center(point)
.zoom(9.0)
.build()
)
}
}

placeAutocompleteUiAdapter.addSearchListener(object : PlaceAutocompleteUiAdapter.SearchListener {

override fun onSuggestionsShown(suggestions: List<PlaceAutocompleteSuggestion>) {
// Nothing to do
}

override fun onSuggestionSelected(suggestion: PlaceAutocompleteSuggestion) {
openPlaceCard(suggestion)
}

override fun onPopulateQueryClick(suggestion: PlaceAutocompleteSuggestion) {
queryEditText.setText(suggestion.name)
}

override fun onError(e: Exception) {
// Nothing to do
}
})

queryEditText.addTextChangedListener(object : TextWatcher {

override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
if (ignoreNextQueryUpdate) {
ignoreNextQueryUpdate = false
} else {
closePlaceCard()
}

lifecycleScope.launchWhenStarted {
placeAutocompleteUiAdapter.search(text.toString())
searchResultsView.isVisible = text.isNotEmpty()
}
}

override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// Nothing to do
}

override fun afterTextChanged(s: Editable) {
// Nothing to do
}
})

if (!isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)) {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
),
PERMISSIONS_REQUEST_LOCATION
)
}
}

private fun reverseGeocoding(point: Point) {
val types: List<PlaceAutocompleteType> = when (mapboxMap.cameraState.zoom) {
in 0.0..4.0 -> REGION_LEVEL_TYPES
in 4.0..6.0 -> DISTRICT_LEVEL_TYPES
in 6.0..12.0 -> LOCALITY_LEVEL_TYPES
else -> ALL_TYPES
}

lifecycleScope.launchWhenStarted {
val response = placeAutocomplete.suggestions(point, PlaceAutocompleteOptions(types = types))
response.onValue { suggestions ->
if (suggestions.isEmpty()) {
showToast(R.string.place_autocomplete_reverse_geocoding_error_message)
} else {
openPlaceCard(suggestions.first())
}
}.onError { error ->
Log.d(LOG_TAG, "Reverse geocoding error", error)
showToast(R.string.place_autocomplete_reverse_geocoding_error_message)
}
}
}

private fun openPlaceCard(suggestion: PlaceAutocompleteSuggestion) {
ignoreNextQueryUpdate = true
queryEditText.setText("")

lifecycleScope.launchWhenStarted {
placeAutocomplete.select(suggestion).onValue { result ->
mapMarkersManager.showMarker(suggestion.coordinate)
searchPlaceView.open(SearchPlace.createFromPlaceAutocompleteResult(result))
queryEditText.hideKeyboard()
searchResultsView.isVisible = false
}.onError { error ->
Log.d(LOG_TAG, "Suggestion selection error", error)
showToast(R.string.place_autocomplete_selection_error)
}
}
}

private fun closePlaceCard() {
searchPlaceView.hide()
mapMarkersManager.clearMarkers()
}

private class MapMarkersManager(mapView: MapView) {

private val mapboxMap = mapView.getMapboxMap()
private val circleAnnotationManager = mapView.annotations.createCircleAnnotationManager(null)
private val markers = mutableMapOf<Long, Point>()

fun clearMarkers() {
markers.clear()
circleAnnotationManager.deleteAll()
}

fun showMarker(coordinate: Point) {
clearMarkers()

val circleAnnotationOptions: CircleAnnotationOptions = CircleAnnotationOptions()
.withPoint(coordinate)
.withCircleRadius(8.0)
.withCircleColor("#ee4e8b")
.withCircleStrokeWidth(2.0)
.withCircleStrokeColor("#ffffff")

val annotation = circleAnnotationManager.create(circleAnnotationOptions)
markers[annotation.id] = coordinate

CameraOptions.Builder()
.center(coordinate)
.padding(MARKERS_INSETS_OPEN_CARD)
.zoom(14.0)
.build().also {
mapboxMap.setCamera(it)
}
}
}

private companion object {

const val PERMISSIONS_REQUEST_LOCATION = 0

const val LOG_TAG = "AutocompleteUiActivity"

val MARKERS_EDGE_OFFSET = dpToPx(64).toDouble()
val PLACE_CARD_HEIGHT = dpToPx(300).toDouble()
val MARKERS_TOP_OFFSET = dpToPx(88).toDouble()

val MARKERS_INSETS_OPEN_CARD = EdgeInsets(
MARKERS_TOP_OFFSET, MARKERS_EDGE_OFFSET, PLACE_CARD_HEIGHT, MARKERS_EDGE_OFFSET
)

val REGION_LEVEL_TYPES = listOf(
PlaceAutocompleteType.AdministrativeUnit.Country,
PlaceAutocompleteType.AdministrativeUnit.Region
)

val DISTRICT_LEVEL_TYPES = REGION_LEVEL_TYPES + listOf(
PlaceAutocompleteType.AdministrativeUnit.Postcode,
PlaceAutocompleteType.AdministrativeUnit.District
)

val LOCALITY_LEVEL_TYPES = DISTRICT_LEVEL_TYPES + listOf(
PlaceAutocompleteType.AdministrativeUnit.Place,
PlaceAutocompleteType.AdministrativeUnit.Locality
)

private val ALL_TYPES = listOf(
PlaceAutocompleteType.Poi,
PlaceAutocompleteType.AdministrativeUnit.Country,
PlaceAutocompleteType.AdministrativeUnit.Region,
PlaceAutocompleteType.AdministrativeUnit.Postcode,
PlaceAutocompleteType.AdministrativeUnit.District,
PlaceAutocompleteType.AdministrativeUnit.Place,
PlaceAutocompleteType.AdministrativeUnit.Locality,
PlaceAutocompleteType.AdministrativeUnit.Neighborhood,
PlaceAutocompleteType.AdministrativeUnit.Street,
PlaceAutocompleteType.AdministrativeUnit.Address,
)
}
}
Was this example helpful?