Android:使用可拖动标记和Google搜索API在地图上选择位置
在Android中开发一个聚焦应用程序的地图,有时我们希望用户选择准确的位置。
主要是我们将使用Google Search API,这真的很强大。
但是,如果我们仔细查看谷歌地图标记,我们将意识到它们可以被拖到精确的位置。
在本教程中,我将继续使用上一个项目,其中我解释了如何在Google地图上显示当前用户位置。
在该项目中,我谈了地图初始化和基本的Android Google地图知识。
在此项目中,我们将被要求在Google控制台启用Google搜索API,并在启用Google Maps API中的相同步骤。
请注意,我们不必使用新的API密钥,使用与地图的键相同。
我们可以从我的github找到这个项目。
克隆或者下载到追随。
打开build.gradle(模块:app)文件和这些依赖项。
implementation 'com.google.android.gms:play-services-location:17.0.0' implementation 'com.google.android.gms:play-services-maps:17.0.0' implementation 'com.google.android.libraries.places:places:2.1.0'
伟大的!首先,我将尝试创建一个用于选择位置的优步克隆。
我们将有一个起点和目的地。
更像是骑行,我们将拥有带有地图的起点和目的地。
这是我主要活动的XML文件。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout 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" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".MainActivity" tools:showIn="@layout/activity_main"> <fragment class="com.google.android.gms.maps.SupportMapFragment" android:name="com.google.android.gms.maps.SupportMapFragment" android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent" <com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/activity_margin" app:cardUseCompatPadding="true" app:cardBackgroundColor="@android:color/white" app:cardCornerRadius="2dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <androidx.appcompat.widget.AppCompatImageView android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" android:src="@drawable/ic_pickup" <androidx.appcompat.widget.AppCompatTextView android:id="@+id/fromLocationTxt" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="center" android:textColor="@color/textColorPrimary" android:text="@string/from_current_location_empty" android:maxLines="1" android:ellipsize="end" <com.google.android.material.button.MaterialButton android:id="@+id/setStartPoint" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAllCaps="false" android:text="@string/set_location" style="@style/Widget.MaterialComponents.Button.TextButton" </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <androidx.appcompat.widget.AppCompatImageView android:layout_width="24dp" android:layout_height="24dp" android:layout_gravity="center" android:src="@drawable/drop_pin" <androidx.appcompat.widget.AppCompatTextView android:id="@+id/toLocationTxt" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="center" android:textColor="@color/textColorPrimary" android:text="@string/to_location_empty" android:maxLines="1" android:ellipsize="end" <com.google.android.material.button.MaterialButton android:id="@+id/setFinishPoint" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAllCaps="false" android:text="@string/set_location" style="@style/Widget.MaterialComponents.Button.TextButton" </LinearLayout> <androidx.appcompat.widget.AppCompatTextView android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="12sp" android:textStyle="italic" android:textColor="@color/textColorPrimary" android:text="@string/drag_info" </LinearLayout> </com.google.android.material.card.MaterialCardView> </RelativeLayout>
在主要活动或者地图活动上,我们初始化Google地图,FusedLocationClient和标记。
请注意,我有一个变量 searchType
保持搜索类型;是否启动位置或者目的地。
private lateinit var googleMap: GoogleMap private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private lateinit var startMarker: Marker private lateinit var finishMarker: Marker private var searchType: Int = 0
大多数功能和方法都在我的前一篇文章中讨论了显示用户当前位置,所以我不会覆盖它。
因此,该想法是通过拖动标记或者搜索,将起点作为当前位置作为当前位置,选择将其更改为另一个位置。
onMapReady()方法初始化地图,并将用户放在地图上的当前位置。
override fun onMapReady(map: GoogleMap?) { googleMap = map?: return if (isPermissionGiven()){ googleMap.isMyLocationEnabled = true googleMap.uiSettings.isMyLocationButtonEnabled = true googleMap.uiSettings.isZoomControlsEnabled = true getCurrentLocation() } else { givePermission() } googleMap.setOnMarkerDragListener(this) }
我们使用位置请求不断刷新用户当前位置。
private fun getCurrentLocation() { val locationRequest = LocationRequest() locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY locationRequest.interval = (10 * 1000).toLong() locationRequest.fastestInterval = 2000 val builder = LocationSettingsRequest.Builder() builder.addLocationRequest(locationRequest) val locationSettingsRequest = builder.build() val result = LocationServices.getSettingsClient(this).checkLocationSettings(locationSettingsRequest) result.addOnCompleteListener { task -> try { val response = task.getResult(ApiException::class.java) if (response!!.locationSettingsStates.isLocationPresent){ getLastLocation() } } catch (exception: ApiException) { when (exception.statusCode) { LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> try { val resolvable = exception as ResolvableApiException resolvable.startResolutionForResult(this, REQUEST_CHECK_SETTINGS) } catch (e: IntentSender.SendIntentException) { } catch (e: ClassCastException) { } LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> { } } } } }
我们现在使用FUSESLOCIONCLIET来检索用户当前位置
private fun getLastLocation() { fusedLocationProviderClient.lastLocation .addOnCompleteListener(this) { task -> if (task.isSuccessful && task.result != null) { val mLastLocation = task.result setStartLocation(mLastLocation!!.latitude, mLastLocation.longitude, "") } else { Toast.makeText(this, "No current location found", Toast.LENGTH_LONG).show() } } } Set the user’s starting point and add marker to it. private fun setStartLocation(lat: Double, lng: Double, addr: String){ var address = "Current address" if (addr.isEmpty()){ val gcd = Geocoder(this, Locale.getDefault()) val addresses: List<Address> try { addresses = gcd.getFromLocation(lat, lng, 1) if (addresses.isNotEmpty()) { address = addresses[0].getAddressLine(0) } } catch (e: IOException) { e.printStackTrace() } } else { address = addr } val icon = BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(this.resources, R.drawable.ic_pickup)) startMarker = googleMap.addMarker( MarkerOptions() .position(LatLng(lat, lng)) .title("Start Location") .snippet("Near $address") .icon(icon) .draggable(true) ) val cameraPosition = CameraPosition.Builder() .target(LatLng(lat, lng)) .zoom(17f) .build() googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)) fromLocationTxt.text = String.format("From: Near %s", address) }
这里最值得注意的点是MarkerOptions()上的拖动(true)和初始化 startMarker
到地图。
将拖动设置为true使标记可拖动,反之亦然是真的。
现在有了这个,在显示标记后,我们可以长时间按下标记,然后将其拖动到首选位置。
此外,请注意,这是可能的,因为如果我们可以在地图初始化上看到,我们添加了标记拖动侦听器:
googleMap.setOnMarkerDragListener(this)
现在要获取新位置的坐标,我们将使用侦听器功能。
我们有三个:
override fun onMarkerDragEnd(p0: Marker?) { if (p0 == startMarker){ setStartLocation(p0.position.latitude, p0.position.longitude, "") } else if (p0 == finishMarker){ setFinishLocation(p0.position.latitude, p0.position.longitude, "") } } override fun onMarkerDragStart(p0: Marker?) { Toast.makeText(this, "Changing location", Toast.LENGTH_SHORT).show() } override fun onMarkerDrag(p0: Marker?) {}
onmarkerdragend()将返回标记。
从标记中,我们可以获得位置并使用地理域尝试获取该地址。
此功能在此中实现 setStartLocation()
功能。
这很简单!
谷歌地图搜索API
这是Google搜索功能的简单调用,以便用户可以输入所需的位置或者点。
使用此API时,请注意谷歌的结算!
在API的新版本中,我们需要首先初始化它。
然后添加字段。
此初始化主要在应用程序类中完成。
这是我们调用它的地方:
if (!Places.isInitialized()) { Places.initialize(applicationContext, BuildConfig.ApiKeyMap) } val fields = listOf(Place.Field.ID, Place.Field.NAME, Place.Field.LAT_LNG) val intent = Autocomplete.IntentBuilder(AutocompleteActivityMode.FULLSCREEN, fields).build(this) if (searchType == 1){ startActivityForResult(intent, CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE) } else { startActivityForResult(intent, DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE) }
在IntentBuilder中,我们有两个选项,AutocompleteActivityMode.fullscreen或者AutocompleteActivityMode.overlay
尝试它们,选择适合人。
接下来是OnactivityResult数据。
我们从意图数据中提取位置并将它们发送到相应的方法。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE){ when (resultCode) { AutocompleteActivity.RESULT_OK -> { val place = Autocomplete.getPlaceFromIntent(data!!) val latLng = place.latLng.toString() val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")")) val loc = location.split(",") setStartLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!) } AutocompleteActivity.RESULT_ERROR -> { val status = Autocomplete.getStatusFromIntent(intent) Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show() } AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show() } } else if (requestCode == DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE){ when (resultCode) { AutocompleteActivity.RESULT_OK -> { val place = Autocomplete.getPlaceFromIntent(data!!) val latLng = place.latLng.toString() val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")")) val loc = location.split(",") setFinishLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!) } AutocompleteActivity.RESULT_ERROR -> { val status = Autocomplete.getStatusFromIntent(intent) Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show() } AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show() } } else if (requestCode == REQUEST_CHECK_SETTINGS){ if (resultCode == Activity.RESULT_OK) { getCurrentLocation() } } }
这只是简单!这是完整的活动:
package com.theitroad.trucksend import android.Manifest import android.app.Activity import android.content.Intent import android.content.IntentSender import android.content.pm.PackageManager import android.graphics.BitmapFactory import android.location.Address import android.location.Geocoder import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import android.view.Menu import android.view.MenuItem import android.widget.Toast import androidx.core.app.ActivityCompat import com.google.android.gms.common.api.ApiException import com.google.android.gms.common.api.ResolvableApiException import com.google.android.gms.location.* import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.OnMapReadyCallback import com.google.android.gms.maps.SupportMapFragment import com.google.android.gms.maps.model.* import com.google.android.libraries.places.api.Places import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.widget.Autocomplete import com.google.android.libraries.places.widget.AutocompleteActivity import com.google.android.libraries.places.widget.model.AutocompleteActivityMode import com.karumi.dexter.Dexter import com.karumi.dexter.PermissionToken import com.karumi.dexter.listener.PermissionDeniedResponse import com.karumi.dexter.listener.PermissionGrantedResponse import com.karumi.dexter.listener.PermissionRequest import com.karumi.dexter.listener.single.PermissionListener import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.content_main.* import java.io.IOException import java.util.* class MainActivity : AppCompatActivity(), OnMapReadyCallback, PermissionListener, GoogleMap.OnMarkerDragListener { companion object { const val REQUEST_CHECK_SETTINGS = 43 const val CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE = 53 const val DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE = 63 } private lateinit var googleMap: GoogleMap private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private lateinit var startMarker: Marker private lateinit var finishMarker: Marker private var searchType: Int = 0 //0->current location, 1->search start location, 2->search destination override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment? mapFragment!!.getMapAsync(this) fusedLocationProviderClient = FusedLocationProviderClient(this) setStartPoint.setOnClickListener { searchType = 1 givePermission() } setFinishPoint.setOnClickListener { searchType = 2 givePermission() } } override fun onMapReady(map: GoogleMap?) { googleMap = map?: return if (isPermissionGiven()){ googleMap.isMyLocationEnabled = true googleMap.uiSettings.isMyLocationButtonEnabled = true googleMap.uiSettings.isZoomControlsEnabled = true getCurrentLocation() } else { givePermission() } googleMap.setOnMarkerDragListener(this) } override fun onMarkerDragEnd(p0: Marker?) { if (p0 == startMarker){ setStartLocation(p0.position.latitude, p0.position.longitude, "") } else if (p0 == finishMarker){ setFinishLocation(p0.position.latitude, p0.position.longitude, "") } } override fun onMarkerDragStart(p0: Marker?) { Toast.makeText(this, "Changing location", Toast.LENGTH_SHORT).show() } override fun onMarkerDrag(p0: Marker?) {} private fun isPermissionGiven(): Boolean{ return ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED } private fun givePermission() { Dexter.withActivity(this) .withPermission(Manifest.permission.ACCESS_FINE_LOCATION) .withListener(this) .check() } override fun onPermissionGranted(response: PermissionGrantedResponse?) { if (searchType == 0){ getCurrentLocation() } else { Toast.makeText(this, resources.getString(R.string.loading), Toast.LENGTH_LONG).show() if (!Places.isInitialized()) { Places.initialize(applicationContext, BuildConfig.ApiKeyMap) } val fields = listOf(Place.Field.ID, Place.Field.NAME, Place.Field.LAT_LNG) val intent = Autocomplete.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields).build(this) if (searchType == 1){ startActivityForResult(intent, CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE) } else { startActivityForResult(intent, DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE) } } } override fun onPermissionRationaleShouldBeShown( permission: PermissionRequest?, token: PermissionToken? ) { token!!.continuePermissionRequest() } override fun onPermissionDenied(response: PermissionDeniedResponse?) { Toast.makeText(this, "Permission required for showing location", Toast.LENGTH_LONG).show() finish() } private fun getCurrentLocation() { val locationRequest = LocationRequest() locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY locationRequest.interval = (10 * 1000).toLong() locationRequest.fastestInterval = 2000 val builder = LocationSettingsRequest.Builder() builder.addLocationRequest(locationRequest) val locationSettingsRequest = builder.build() val result = LocationServices.getSettingsClient(this).checkLocationSettings(locationSettingsRequest) result.addOnCompleteListener { task -> try { val response = task.getResult(ApiException::class.java) if (response!!.locationSettingsStates.isLocationPresent){ getLastLocation() } } catch (exception: ApiException) { when (exception.statusCode) { LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> try { val resolvable = exception as ResolvableApiException resolvable.startResolutionForResult(this, REQUEST_CHECK_SETTINGS) } catch (e: IntentSender.SendIntentException) { } catch (e: ClassCastException) { } LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> { } } } } } private fun getLastLocation() { fusedLocationProviderClient.lastLocation .addOnCompleteListener(this) { task -> if (task.isSuccessful && task.result != null) { val mLastLocation = task.result setStartLocation(mLastLocation!!.latitude, mLastLocation.longitude, "") } else { Toast.makeText(this, "No current location found", Toast.LENGTH_LONG).show() } } } private fun setStartLocation(lat: Double, lng: Double, addr: String){ var address = "Current address" if (addr.isEmpty()){ val gcd = Geocoder(this, Locale.getDefault()) val addresses: List<Address> try { addresses = gcd.getFromLocation(lat, lng, 1) if (addresses.isNotEmpty()) { address = addresses[0].getAddressLine(0) } } catch (e: IOException) { e.printStackTrace() } } else { address = addr } val icon = BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(this.resources, R.drawable.ic_pickup)) startMarker = googleMap.addMarker( MarkerOptions() .position(LatLng(lat, lng)) .title("Start Location") .snippet("Near $address") .icon(icon) .draggable(true) ) val cameraPosition = CameraPosition.Builder() .target(LatLng(lat, lng)) .zoom(17f) .build() googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)) fromLocationTxt.text = String.format("From: Near %s", address) } private fun setFinishLocation(lat: Double, lng: Double, addr: String){ var address = "Destination address" if (addr.isEmpty()){ val gcd = Geocoder(this, Locale.getDefault()) val addresses: List<Address> try { addresses = gcd.getFromLocation(lat, lng, 1) if (addresses.isNotEmpty()) { address = addresses[0].getAddressLine(0) } } catch (e: IOException) { e.printStackTrace() } } else { address = addr } val icon = BitmapDescriptorFactory.fromBitmap(BitmapFactory.decodeResource(this.resources, R.drawable.drop_pin)) finishMarker = googleMap.addMarker( MarkerOptions() .position(LatLng(lat, lng)) .title("Finish Location") .snippet("Near $address") .icon(icon) .draggable(true) ) val cameraPosition = CameraPosition.Builder() .target(LatLng(lat, lng)) .zoom(17f) .build() googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)) toLocationTxt.text = String.format("To: Near %s", address) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == CURRENT_PLACE_AUTOCOMPLETE_REQUEST_CODE){ when (resultCode) { AutocompleteActivity.RESULT_OK -> { val place = Autocomplete.getPlaceFromIntent(data!!) val latLng = place.latLng.toString() val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")")) val loc = location.split(",") setStartLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!) } AutocompleteActivity.RESULT_ERROR -> { val status = Autocomplete.getStatusFromIntent(intent) Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show() } AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show() } } else if (requestCode == DESTINATION_PLACE_AUTOCOMPLETE_REQUEST_CODE){ when (resultCode) { AutocompleteActivity.RESULT_OK -> { val place = Autocomplete.getPlaceFromIntent(data!!) val latLng = place.latLng.toString() val location = latLng.substring(latLng.indexOf("(") + 1, latLng.indexOf(")")) val loc = location.split(",") setFinishLocation(loc[0].toDouble(), loc[1].toDouble(), place.name!!) } AutocompleteActivity.RESULT_ERROR -> { val status = Autocomplete.getStatusFromIntent(intent) Toast.makeText(this, status.statusMessage, Toast.LENGTH_SHORT).show() } AutocompleteActivity.RESULT_CANCELED -> Toast.makeText(this, getString(R.string.request_cancelled), Toast.LENGTH_SHORT).show() } } else if (requestCode == REQUEST_CHECK_SETTINGS){ if (resultCode == Activity.RESULT_OK) { getCurrentLocation() } } } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.menu_main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.action_settings -> true else -> super.onOptionsItemSelected(item) } } }