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. The dependencies can be found here.The examples use View binding.See setup documention if necessary.

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?