Basic search + Maps SDK integration
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.
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:ignore="MergeRootFrame"
>
<com.mapbox.maps.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:mapbox_logoGravity="bottom"
/>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:theme="@style/ToolbarTheme"
/>
<com.mapbox.search.ui.view.SearchResultsView
android:id="@+id/search_results_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?actionBarSize"
android:clipToPadding="false"
android:paddingTop="22dp"
android:paddingBottom="22dp"
/>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/search_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?actionBarSize"
>
<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>
</FrameLayout>
package com.mapbox.search.sample
import android.Manifest
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.view.isVisible
import com.mapbox.android.core.location.LocationEngine
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.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.locationcomponent.OnIndicatorPositionChangedListener
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.search.ApiType
import com.mapbox.search.ResponseInfo
import com.mapbox.search.SearchEngine
import com.mapbox.search.SearchEngineSettings
import com.mapbox.search.offline.OfflineResponseInfo
import com.mapbox.search.offline.OfflineSearchEngine
import com.mapbox.search.offline.OfflineSearchEngineSettings
import com.mapbox.search.offline.OfflineSearchResult
import com.mapbox.search.record.HistoryRecord
import com.mapbox.search.result.SearchResult
import com.mapbox.search.result.SearchSuggestion
import com.mapbox.search.sample.api.AddressAutofillKotlinExampleActivity
import com.mapbox.search.sample.api.CategorySearchJavaExampleActivity
import com.mapbox.search.sample.api.CategorySearchKotlinExampleActivity
import com.mapbox.search.sample.api.CustomIndexableDataProviderJavaExample
import com.mapbox.search.sample.api.CustomIndexableDataProviderKotlinExample
import com.mapbox.search.sample.api.DiscoverJavaExampleActivity
import com.mapbox.search.sample.api.DiscoverKotlinExampleActivity
import com.mapbox.search.sample.api.FavoritesDataProviderJavaExample
import com.mapbox.search.sample.api.FavoritesDataProviderKotlinExample
import com.mapbox.search.sample.api.ForwardGeocodingBatchResolvingJavaExampleActivity
import com.mapbox.search.sample.api.ForwardGeocodingBatchResolvingKotlinExampleActivity
import com.mapbox.search.sample.api.ForwardGeocodingJavaExampleActivity
import com.mapbox.search.sample.api.ForwardGeocodingKotlinExampleActivity
import com.mapbox.search.sample.api.HistoryDataProviderJavaExample
import com.mapbox.search.sample.api.HistoryDataProviderKotlinExample
import com.mapbox.search.sample.api.JapanSearchJavaExampleActivity
import com.mapbox.search.sample.api.JapanSearchKotlinExampleActivity
import com.mapbox.search.sample.api.OfflineReverseGeocodingJavaExampleActivity
import com.mapbox.search.sample.api.OfflineReverseGeocodingKotlinExampleActivity
import com.mapbox.search.sample.api.OfflineSearchJavaExampleActivity
import com.mapbox.search.sample.api.OfflineSearchKotlinExampleActivity
import com.mapbox.search.sample.api.PlaceAutocompleteKotlinExampleActivity
import com.mapbox.search.sample.api.ReverseGeocodingJavaExampleActivity
import com.mapbox.search.sample.api.ReverseGeocodingKotlinExampleActivity
import com.mapbox.search.ui.adapter.engines.SearchEngineUiAdapter
import com.mapbox.search.ui.view.CommonSearchViewConfiguration
import com.mapbox.search.ui.view.DistanceUnitType
import com.mapbox.search.ui.view.SearchMode
import com.mapbox.search.ui.view.SearchResultsView
import com.mapbox.search.ui.view.place.SearchPlace
import com.mapbox.search.ui.view.place.SearchPlaceBottomSheetView
class MainActivity : AppCompatActivity() {
private lateinit var locationEngine: LocationEngine
private lateinit var toolbar: Toolbar
private lateinit var searchView: SearchView
private lateinit var searchResultsView: SearchResultsView
private lateinit var searchEngineUiAdapter: SearchEngineUiAdapter
private lateinit var searchPlaceView: SearchPlaceBottomSheetView
private lateinit var mapView: MapView
private lateinit var mapMarkersManager: MapMarkersManager
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
when {
!searchPlaceView.isHidden() -> {
mapMarkersManager.clearMarkers()
searchPlaceView.hide()
}
mapMarkersManager.hasMarkers -> {
mapMarkersManager.clearMarkers()
}
else -> {
if (BuildConfig.DEBUG) {
error("This OnBackPressedCallback should not be enabled")
}
Log.i("SearchApiExample", "This OnBackPressedCallback should not be enabled")
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
onBackPressedDispatcher.addCallback(onBackPressedCallback)
locationEngine = LocationEngineProvider.getBestLocationEngine(applicationContext)
mapView = findViewById(R.id.map_view)
mapView.getMapboxMap().also { mapboxMap ->
mapboxMap.loadStyleUri(getMapStyleUri())
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)
mapMarkersManager.onMarkersChangeListener = {
updateOnBackPressedCallbackEnabled()
}
toolbar = findViewById(R.id.toolbar)
toolbar.apply {
title = getString(R.string.simple_ui_toolbar_title)
setSupportActionBar(this)
}
val apiType = if (BuildConfig.ENABLE_SBS) {
ApiType.SBS
} else {
ApiType.GEOCODING
}
searchResultsView = findViewById<SearchResultsView>(R.id.search_results_view).apply {
initialize(
SearchResultsView.Configuration(CommonSearchViewConfiguration(DistanceUnitType.IMPERIAL))
)
isVisible = false
}
val searchEngine = SearchEngine.createSearchEngineWithBuiltInDataProviders(
apiType = apiType,
settings = SearchEngineSettings(getString(R.string.mapbox_access_token))
)
val offlineSearchEngine = OfflineSearchEngine.create(
OfflineSearchEngineSettings(getString(R.string.mapbox_access_token))
)
searchEngineUiAdapter = SearchEngineUiAdapter(
view = searchResultsView,
searchEngine = searchEngine,
offlineSearchEngine = offlineSearchEngine,
)
searchEngineUiAdapter.searchMode = SearchMode.AUTO
searchEngineUiAdapter.addSearchListener(object : SearchEngineUiAdapter.SearchListener {
override fun onSuggestionsShown(suggestions: List<SearchSuggestion>, responseInfo: ResponseInfo) {
// Nothing to do
}
override fun onSearchResultsShown(
suggestion: SearchSuggestion,
results: List<SearchResult>,
responseInfo: ResponseInfo
) {
closeSearchView()
mapMarkersManager.showMarkers(results.map { it.coordinate })
}
override fun onOfflineSearchResultsShown(results: List<OfflineSearchResult>, responseInfo: OfflineResponseInfo) {
// Nothing to do
}
override fun onSuggestionSelected(searchSuggestion: SearchSuggestion): Boolean {
return false
}
override fun onSearchResultSelected(searchResult: SearchResult, responseInfo: ResponseInfo) {
closeSearchView()
searchPlaceView.open(SearchPlace.createFromSearchResult(searchResult, responseInfo))
mapMarkersManager.showMarker(searchResult.coordinate)
}
override fun onOfflineSearchResultSelected(searchResult: OfflineSearchResult, responseInfo: OfflineResponseInfo) {
closeSearchView()
searchPlaceView.open(SearchPlace.createFromOfflineSearchResult(searchResult))
mapMarkersManager.showMarker(searchResult.coordinate)
}
override fun onError(e: Exception) {
Toast.makeText(applicationContext, "Error happened: $e", Toast.LENGTH_SHORT).show()
}
override fun onHistoryItemClick(historyRecord: HistoryRecord) {
closeSearchView()
searchPlaceView.open(SearchPlace.createFromIndexableRecord(historyRecord, distanceMeters = null))
locationEngine.userDistanceTo(this@MainActivity, historyRecord.coordinate) { distance ->
distance?.let {
searchPlaceView.updateDistance(distance)
}
}
mapMarkersManager.showMarker(historyRecord.coordinate)
}
override fun onPopulateQueryClick(suggestion: SearchSuggestion, responseInfo: ResponseInfo) {
if (::searchView.isInitialized) {
searchView.setQuery(suggestion.name, true)
}
}
override fun onFeedbackItemClick(responseInfo: ResponseInfo) {
// Not implemented
}
})
searchPlaceView = findViewById(R.id.search_place_view)
searchPlaceView.initialize(CommonSearchViewConfiguration(DistanceUnitType.IMPERIAL))
searchPlaceView.addOnCloseClickListener {
mapMarkersManager.clearMarkers()
searchPlaceView.hide()
}
searchPlaceView.addOnNavigateClickListener { searchPlace ->
startActivity(geoIntent(searchPlace.coordinate))
}
searchPlaceView.addOnShareClickListener { searchPlace ->
startActivity(shareIntent(searchPlace))
}
searchPlaceView.addOnFeedbackClickListener { _, _ ->
// Not implemented
}
searchPlaceView.addOnBottomSheetStateChangedListener { _, _ ->
updateOnBackPressedCallbackEnabled()
}
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 updateOnBackPressedCallbackEnabled() {
onBackPressedCallback.isEnabled = !searchPlaceView.isHidden() || mapMarkersManager.hasMarkers
}
private fun closeSearchView() {
toolbar.collapseActionView()
searchView.setQuery("", false)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_activity_options_menu, menu)
val searchActionView = menu.findItem(R.id.action_search)
searchActionView.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchPlaceView.hide()
searchResultsView.isVisible = true
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
searchResultsView.isVisible = false
return true
}
})
searchView = searchActionView.actionView as SearchView
searchView.queryHint = getString(R.string.query_hint)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
searchEngineUiAdapter.search(newText)
return false
}
})
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.open_address_autofill_ui_example -> {
startActivity(Intent(this, AddressAutofillUiActivity::class.java))
true
}
R.id.open_address_autofill_example -> {
startActivity(Intent(this, AddressAutofillKotlinExampleActivity::class.java))
true
}
R.id.open_discover_ui_example -> {
startActivity(Intent(this, DiscoverActivity::class.java))
true
}
R.id.open_discover_kotlin_example -> {
startActivity(Intent(this, DiscoverKotlinExampleActivity::class.java))
true
}
R.id.open_discover_java_example -> {
startActivity(Intent(this, DiscoverJavaExampleActivity::class.java))
true
}
R.id.open_place_autocomplete_ui_example -> {
startActivity(Intent(this, PlaceAutocompleteUiActivity::class.java))
true
}
R.id.open_place_autocomplete_kotlin_example -> {
startActivity(Intent(this, PlaceAutocompleteKotlinExampleActivity::class.java))
true
}
R.id.open_custom_data_provider_kt_example -> {
startActivity(Intent(this, CustomIndexableDataProviderKotlinExample::class.java))
true
}
R.id.open_custom_data_provider_java_example -> {
startActivity(Intent(this, CustomIndexableDataProviderJavaExample::class.java))
true
}
R.id.custom_theme_example -> {
startActivity(Intent(this, CustomThemeActivity::class.java))
true
}
R.id.open_forward_geocoding_kt_example -> {
startActivity(Intent(this, ForwardGeocodingKotlinExampleActivity::class.java))
true
}
R.id.open_forward_geocoding_java_example -> {
startActivity(Intent(this, ForwardGeocodingJavaExampleActivity::class.java))
true
}
R.id.open_forward_geocoding_batch_resolving_kt_example -> {
startActivity(Intent(this, ForwardGeocodingBatchResolvingKotlinExampleActivity::class.java))
true
}
R.id.open_forward_geocoding_batch_resolving_java_example -> {
startActivity(Intent(this, ForwardGeocodingBatchResolvingJavaExampleActivity::class.java))
true
}
R.id.open_reverse_geocoding_kt_example -> {
startActivity(Intent(this, ReverseGeocodingKotlinExampleActivity::class.java))
true
}
R.id.open_reverse_geocoding_java_example -> {
startActivity(Intent(this, ReverseGeocodingJavaExampleActivity::class.java))
true
}
R.id.open_japan_search_kt_example -> {
startActivity(Intent(this, JapanSearchKotlinExampleActivity::class.java))
true
}
R.id.open_japan_search_java_example -> {
startActivity(Intent(this, JapanSearchJavaExampleActivity::class.java))
true
}
R.id.open_category_search_kt_example -> {
startActivity(Intent(this, CategorySearchKotlinExampleActivity::class.java))
true
}
R.id.open_category_search_java_example -> {
startActivity(Intent(this, CategorySearchJavaExampleActivity::class.java))
true
}
R.id.open_offline_search_java_example -> {
startActivity(Intent(this, OfflineSearchJavaExampleActivity::class.java))
true
}
R.id.open_offline_search_kt_example -> {
startActivity(Intent(this, OfflineSearchKotlinExampleActivity::class.java))
true
}
R.id.open_offline_reverse_geocoding_java_example -> {
startActivity(Intent(this, OfflineReverseGeocodingJavaExampleActivity::class.java))
true
}
R.id.open_offline_reverse_geocoding_kt_example -> {
startActivity(Intent(this, OfflineReverseGeocodingKotlinExampleActivity::class.java))
true
}
R.id.open_history_data_provider_java_example -> {
startActivity(Intent(this, HistoryDataProviderJavaExample::class.java))
true
}
R.id.open_history_data_provider_kt_example -> {
startActivity(Intent(this, HistoryDataProviderKotlinExample::class.java))
true
}
R.id.open_favorites_data_provider_java_example -> {
startActivity(Intent(this, FavoritesDataProviderJavaExample::class.java))
true
}
R.id.open_favorites_data_provider_kt_example -> {
startActivity(Intent(this, FavoritesDataProviderKotlinExample::class.java))
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun getMapStyleUri(): String {
return when (val darkMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> Style.DARK
Configuration.UI_MODE_NIGHT_NO,
Configuration.UI_MODE_NIGHT_UNDEFINED -> Style.MAPBOX_STREETS
else -> error("Unknown mode: $darkMode")
}
}
private class MapMarkersManager(mapView: MapView) {
private val mapboxMap = mapView.getMapboxMap()
private val circleAnnotationManager = mapView.annotations.createCircleAnnotationManager(null)
private val markers = mutableMapOf<Long, Point>()
var onMarkersChangeListener: (() -> Unit)? = null
val hasMarkers: Boolean
get() = markers.isNotEmpty()
fun clearMarkers() {
markers.clear()
circleAnnotationManager.deleteAll()
}
fun showMarker(coordinate: Point) {
showMarkers(listOf(coordinate))
}
fun showMarkers(coordinates: List<Point>) {
clearMarkers()
if (coordinates.isEmpty()) {
onMarkersChangeListener?.invoke()
return
}
coordinates.forEach { coordinate ->
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
}
if (coordinates.size == 1) {
CameraOptions.Builder()
.center(coordinates.first())
.padding(MARKERS_INSETS_OPEN_CARD)
.zoom(14.0)
.build()
} else {
mapboxMap.cameraForCoordinates(
coordinates, MARKERS_INSETS, bearing = null, pitch = null
)
}.also {
mapboxMap.setCamera(it)
}
onMarkersChangeListener?.invoke()
}
}
private companion object {
val MARKERS_EDGE_OFFSET = dpToPx(64).toDouble()
val PLACE_CARD_HEIGHT = dpToPx(300).toDouble()
val MARKERS_INSETS = EdgeInsets(
MARKERS_EDGE_OFFSET, MARKERS_EDGE_OFFSET, MARKERS_EDGE_OFFSET, MARKERS_EDGE_OFFSET
)
val MARKERS_INSETS_OPEN_CARD = EdgeInsets(
MARKERS_EDGE_OFFSET, MARKERS_EDGE_OFFSET, PLACE_CARD_HEIGHT, MARKERS_EDGE_OFFSET
)
const val PERMISSIONS_REQUEST_LOCATION = 0
}
}
この{Type}は役に立ちましたか?