Android 谷歌地图
- 前言
- 正文
- 一、设置Google Cloud 项目
- 二、项目配置
- ① 设置SDK
- ② 配置API密钥
- ③ 配置AndroidManifest.xml
- 三、添加地图
- 四、定位当前
- ① 请求定位权限
- ② 我的位置控件
- ③ 获取当前位置
- 五、配置地图
- ① xml配置地图
- ② 代码配置地图
- ③ 地图点击事件
- ④ 管理Marker
- 六、地址位置编码
- ① 坐标转地址
- ② 地址转坐标
- 七、源码
前言
在国内你选择的SDK可以是高德、百度、腾讯等,但在国外,你首选肯定是谷歌,因此要进行Google地图的开发你首先要解决下面三个问题
- VPN
- Google账号
- 信用卡
- American Express(美国运通卡)
- Discover(美国发现卡)
- JCB(Japan Credit Bureau,日本国际信用卡)
- MasterCard(万事达)
- VISA(维萨)
正文
首先我们进入Google的地图开发平台,点击:Google Maps进入,建议你使用Google Chrome进行访问。
一、设置Google Cloud 项目
点击这里的创建新项目按钮。
输入名字后,点击创建。
然后我们进入API和服务,然后你就会发现你需要设置账号信息和付款验证信息,这一步还挺麻烦的,主要是那个卡的信息,在前面我已经提过了。 在你通过账号信息验证之后就可以创建API秘钥了,创建的API之后需要对应使用应用的包名和SHA1证书指纹,一个API秘钥可以增加多个App进行配置,只有配置之后的App才能通过此API秘钥访问Google Maps。
在你配置好之后你就会得到一个API密钥,这个密钥我们需要在项目中配置好,下面进入项目。
二、项目配置
一般情况这里是要进入项目的创建和配置了,而因为Google这边比较特殊,你可能需要先上架一个应用上去,我这边的正式版的,你可以试试测试版行不行,有应用之后我们就可以通过选择应用,使配置的API密钥去生效。
我之前在使用的时候就遇到过一个指纹不对的情况,结果发现你的应用有两个指纹,你可以理解为测试版和正式版,如果你遇到这个情况,那么你换一下试试看。
① 设置SDK
首先你要检查一下你的项目是否导入google()
、mavenCentral()
这两个仓库,如果没有的话你就需要导入了,有则不用管,而根据你所使用的Gradle的不同,你配置这两个仓库的地方也不一样,如果Gradle是7.4以上的则在settings.gradle
文件中配置,否则在工程级build.gradle
配置,我这边就是工程级build.gradle
,如下所示:
repositories {
google()
mavenCentral()
}
然后我们找到需要使用地图的模块,例如app模块,找到该模块下的build.gradle
,在里面中dependencies{}
闭包中添加如下依赖:
// Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:19.0.0'
同时我们注意配置一下buildFeatures
,在模块级 build.gradle
的 buildFeatures
部分中 或 build.gradle
文件中,请添加 BuildConfig
类,该类可用于 访问此过程后面部分定义的元数据值:
buildFeatures {
buildConfig true
}
这里你可以先Sync Now同步一下,也可以不急,在配置了API密钥之后再同步。
② 配置API密钥
基于Google上推荐的配置方式,我们这里首先在打开工程的build.gradle,在里面添加
buildscript {
dependencies {
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
}
}
这是Android 版 Secret Gradle 插件,然后打开app模块下build.gradle
,在plugins{}
闭包中添加如下代码:
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
然后我们将build.gradle
文件中,设置 targetSdk
和 compileSdk
到 34,如果不能到34,那么你的相关的依赖就需要降低一些版本,否则会出现同步失败的情况,这是你可以Sync Now
同步一下了。
接着我们在功能的根目录下创建一个secrets.properties
文件,请注意它和你的工程级build.gradle
是同级的,在这个文件里面配置如下代码:
MAPS_API_KEY=YOUR_API_KEY
注意将YOUR_API_KEY
,替换为你实际申请到的API密钥,然后保存文件,然后同样是这个目录,我们再创建一个local.defaults.properties
文件,里面的代码如下所示:
MAPS_API_KEY=DEFAULT_API_KEY
此文件的作用是为 API 密钥提供备用位置,以免在找不到 secrets.properties
文件的情况下构建失败。如果您是从省略 secrets.properties
的版本控制系统中克隆应用,而您还没有在本地创建 secrets.properties
文件来提供 API 密钥,就可能会出现构建失败。然后保存文件。
接着我们打开 AndroidManifest.xml
文件,在<application>
标签中添加如下代码:
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
最后我们在app模块下的android{}
闭包中增加一个secrets
属性,如果该属性不存在,代码如下所示:
secrets {
propertiesFileName = "secrets.properties"
defaultPropertiesFileName = "local.defaults.properties"
ignoreList.add("keyToIgnore")
ignoreList.add("sdk.*")
}
下面再Sync Now
同步一下。
③ 配置AndroidManifest.xml
首先配置Google Play 服务版本号,在 application
标签中添加以下声明。该操作会嵌入编译应用时所用 Google Play 服务的版本,代码如下所示:
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
然后再增加一个Apache HTTP 旧版库,代码如下所示:
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
最后我们再配置一下需要使用到的权限,
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
到此为止配置工作就完成了,你可以先不急着添加地图,先运行一下试试看,有没有报错或者其他的问题,如果没有问题再进行下一步。
三、添加地图
首先我们在工程中创建一个map包,里面新建一个GoogleMapActivity。
完成创建之后,我们用上ViewBinding,代码如下所示:
class GoogleMapActivity : AppCompatActivity() {
private lateinit var binding: ActivityGoogleMapBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityGoogleMapBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
}
}
注意自行导包,并且设置一个可以进入这个页面的入口,比如点击一个按钮跳转到这个页面来。下面我们配置XML,打开activity_google_map.xml
,修改后代码如下所示:
<?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:map="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
这里我们以静态方式添加 fragment,在用于处理地图的 activity 的布局文件中,添加名称声明 xmlns:map="http://schemas.android.com/apk/res-auto"
。完成此操作后即可使用 maps 自定义 XML 属性。在后面我们就可以直接在xml中通过map去设置地图的一些属性了。将 android:name
属性设置为com.google.android.gms.maps.SupportMapFragment
,这是必须要做的事情。
接下来回到GoogleMapActivity
,首先我们创建一个initView()
函数,代码如下:
/**
* 初始化视图
*/
private fun initView() {
val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
这里获取 fragment 的句柄并注册回调函数,GoogleMapActivity需要实现OnMapReadyCallback
接口,重写里面的onMapReady()
函数,在这个方法中我们添加一个Marker,代码如下所示:
/**
* 地图就绪
*/
override fun onMapReady(googleMap: GoogleMap) {
googleMap.addMarker(
MarkerOptions()
.position(LatLng(0.0, 0.0))
.title("Marker")
)
}
最后要在onCreate()函数中调用initView()函数,最终代码如下图所示:
下面我们运行一下看看
OK,你会看到出现了这个Marker,就是我们所设置的地方,如果你没有加载出这个画面,那么检查一下你的控制台,看看有没有相关的错误日志,再根据日志判断具体问题,一般都是配置的问题,请根据一、二步骤进行检查。
四、定位当前
上述的内容对你毫无难度,我们继续往下走,现在地图加载出来了,我们最实际的想法就是定位当前所在位置,那么要怎么做呢,这里分为两种方式,无论那种方式,我们都需要先获取位置权限。
① 请求定位权限
在Android6.0及以上版本定位权限光在AndroidManifest.xml
配置还不够,还需要动态请求,下面我们在GoogleMapActivity
中完成这一代码。
首先声明变量,如下所示:
private val TAG: String = GoogleMapActivity::class.java.simpleName
// 权限请求码
private val LOCATION_PERMISSION_REQUEST_CODE: Int = 9527
// 地图
private lateinit var map: GoogleMap
这里的map我们需要在onMapReady()函数中进行赋值,
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
// 检查权限
checkPermission()
}
通过我们增加一个检查权限的函数,也就地图就绪之后我们就检查权限,代码如下所示:
/**
* 检查权限
*/
private fun checkPermission() {
// 检查当前是否拥有精确位置或粗略位置权限
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
// 权限已授予,可以进行定位操作
Log.d(TAG, "checkPermission: 权限已授予")
configMap()
} else {
Log.d(TAG, "checkPermission: 请求权限")
// 请求权限
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
),
LOCATION_PERMISSION_REQUEST_CODE
)
}
}
这里的代码很常规就是,先检查有没有相关权限,有就配置地图,没有就请求权限。然后写一个配置地图的函数,代码如下:
/**
* 地图配置
*/
private fun configMap() {
Log.d(TAG, "configMap: 地图配置")
map.addMarker(
MarkerOptions()
.position(LatLng(0.0, 0.0))
.title("Marker")
)
}
最后重写onRequestPermissionsResult()
函数,捕获权限请求结果,代码如下所示:
/**
* 权限请求结果
*/
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
LOCATION_PERMISSION_REQUEST_CODE -> {
// 如果请求被取消,则结果数组为空
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限被授予,可以进行定位操作
Log.d(TAG, "onRequestPermissionsResult: 权限被授予")
configMap()
} else {
// 权限被拒绝,无法进行定位操作
Log.d(TAG, "onRequestPermissionsResult: 权限被拒绝")
Toast.makeText(this, "拒绝将无法使用定位功能", Toast.LENGTH_SHORT).show()
finish()
}
}
}
}
获取权限则配置地图,拒绝权限直接提示一下就退出了,下面我们运行一下:
再看看控制台日志:
OK,没有问题,现在权限的问题我们就解决了,下面进行定位。
② 我的位置控件
要定位到当前位置,我们可以使用Google地图中的自带控件,修改configMap()
函数,代码如下所示:
/**
* 地图配置
*/
@SuppressLint("MissingPermission")
private fun configMap() {
Log.d(TAG, "configMap: 地图配置")
map.addMarker(
MarkerOptions()
.position(LatLng(0.0, 0.0))
.title("Marker")
)
map.isMyLocationEnabled = true
// 当前位置图标的点击事件
map.setOnMyLocationButtonClickListener {
Log.d(TAG, "configMap: 点击位置图标")
return@setOnMyLocationButtonClickListener false
}
// 定位后的蓝点点击事件
map.setOnMyLocationClickListener { location ->
Log.d(TAG, "configMap: 点击我的位置 $location")
Toast.makeText(this, "Current location:\n$location", Toast.LENGTH_LONG).show()
}
}
注意要加上这个@SuppressLint("MissingPermission")
,不然会检查map.isMyLocationEnabled = true
是否通过权限判断,这里我们在地图上启用“我的位置”图层。则地图上就会出现一个定位当前位置的控件,出现在右上角,setOnMyLocationButtonClickListener
则是这个控件的点击监听,这里返回false,则点击之后就会移动地图中心到当前设备所在位置,setOnMyLocationClickListener
则是定位后的蓝色点的点击事件,这里运行之后就会看到。
你会看到右上角的定位按钮,点击就可以了,控制台如下所示:
③ 获取当前位置
首先声明变量
// 地图
private lateinit var map: GoogleMap
// Places API 的入口点。
private lateinit var placesClient: PlacesClient
// 融合位置信息提供程序的入口点。
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
// 最后已知位置
private var lastKnownLocation: Location? = null
companion object {
private val TAG = GoogleMapActivity::class.java.simpleName
// 默认缩放
private const val DEFAULT_ZOOM = 15
// 权限请求码
private const val LOCATION_PERMISSION_REQUEST_CODE = 9527
// 未授予位置权限时使用的默认位置(澳大利亚悉尼)和默认缩放。
private val defaultLocation = LatLng(-33.8523341, 151.2106085)
}
在initView()
函数中增加如下代码:
// 构造 PlacesClient
Places.initialize(applicationContext, BuildConfig.MAPS_API_KEY)
placesClient = Places.createClient(this)
// 构造 FusedLocationProviderClient。
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
添加位置如下图所示:
下面我们需要写一个getCurrentLocation()
函数获取当前位置,代码如下所示:
/**
* 获取当前位置
*/
@SuppressLint("MissingPermission")
private fun getCurrentLocation() {
Log.d(TAG, "getCurrentLocation: 获取当前位置")
fusedLocationProviderClient.lastLocation.addOnCompleteListener { task ->
// 获取当前位置未成功
if (!task.isSuccessful) {
Log.d(TAG, "Current location is null. Using defaults.")
Log.e(TAG, "Exception: %s", task.exception)
// 设置默认位置
changeMapCenter(defaultLocation)
return@addOnCompleteListener
}
lastKnownLocation = task.result
if (lastKnownLocation == null) return@addOnCompleteListener
// 移动地图到当前位置
changeMapCenter(LatLng(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude))
}
}
这里有一个changeMapCenter()函数,用于改变地图中心,代码如下所示:
/**
* 改变地图中心
*/
private fun changeMapCenter(latLng: LatLng) {
map.addMarker(MarkerOptions()
.title("Marker")
.position(latLng))
// 地图中移动到经纬度处
map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM.toFloat()))
}
最后我们在configMap()
函数中调用getCurrentLocation()
函数,同时去掉之前的默认Marker,如下图所示:
下面你运行一下就会自动定位到当前设备所在位置了,这个位置不定义完全正确,有一些偏差,控制台日志如下图所示:
五、配置地图
① xml配置地图
完成定位之后,我们可以对地图进行一些配置,我们可以通过xml去配置。
map:cameraTilt="30"
map:uiRotateGestures="true"
map:uiZoomControls="true"
-
cameraTilt
将地图倾斜度设置为 30 -
uiRotateGestures
启用旋转手势控件 -
uiZoomControls
启用缩放控件
添加位置如下图所示:
XML属性还有其他的设置:
- mapType - 要显示的地图类型。有效值包括:none、normal、hybrid、satellite 和 terrain。
- cameraTargetLat、cameraTargetLng、cameraZoom、cameraBearing、cameraTilt - 镜头的初始位置。
- uiZoomControls、uiCompass - 用于指定是否显示缩放控件和罗盘。
- uiZoomGestures、uiScrollGestures、uiRotateGestures、uiTiltGestures - 用于指定是否启用特定手势。
- zOrderOnTop - 用于指明地图视图的表面是否叠加显示在地图窗口、地图控件和窗口中的任何对象上。
- useViewLifecycle - 此属性必须与 SupportMapFragment 对象一起使用才有效,它用于指定是否应将地图的生命周期与 fragment 的视图或 fragment 本身关联。
- liteMode - 如果要启用精简模式,则为 true;否则为 false。
② 代码配置地图
xml可以设置的,同样可以通过代码设置。就需要用到GoogleMapOptions
和UiSettings
,如果你使用的是动态加载的地图,那么就使用GoogleMapOptions
的方式,如果是静态加载的地图就使用UiSettings
,这里我们使用UiSettings
去设置地图,修改一下configMap()
中的代码,如下图所示:
/**
* 地图配置
*/
@SuppressLint("MissingPermission")
private fun configMap() {
Log.d(TAG, "configMap: 地图配置")
map.apply {
isMyLocationEnabled = true // 地图上启用“我的位置”图层
// 当前位置图标的点击事件
setOnMyLocationButtonClickListener {
Log.d(TAG, "configMap: 点击位置图标")
return@setOnMyLocationButtonClickListener false
}
// 定位后的蓝点点击事件
setOnMyLocationClickListener { location ->
Log.d(TAG, "configMap: 点击我的位置 $location")
Toast.makeText(this@GoogleMapActivity, "Current location:\n$location", Toast.LENGTH_LONG).show()
}
// 地图设置
uiSettings.apply {
isZoomControlsEnabled = true // 显示缩放按钮
isMyLocationButtonEnabled = true // 显示定位按钮
isCompassEnabled = true // 显示指南针
isMapToolbarEnabled = true // 显示地图工具栏
isRotateGesturesEnabled = true // 允许旋转手势
isScrollGesturesEnabled = true // 允许滚动手势
isTiltGesturesEnabled = true // 允许倾斜手势
isZoomGesturesEnabled = true // 允许缩放手势
isScrollGesturesEnabledDuringRotateOrZoom = true // 允许在旋转或缩放时滚动手势
isIndoorLevelPickerEnabled = true // 显示室内层选择器
}
}
// 获取当前位置
getCurrentLocation()
}
主要是注意uiSettings里面的配置,可以自行运行看配置效果。
③ 地图点击事件
关于地图的事件我们主要讲述点击事件,比如我们点击哪里就移动地图到哪里,这是很常用的一个功能,实现起来也很简单,在configMap()
函数中添加如下代码:
// 地图点击事件
setOnMapClickListener { latLng ->
changeMapCenter(latLng)
}
这里就做到了,点击哪里移动到哪里,因为在changeMapCenter()
函数中,对于定位点进行添加Marker,所以,如果你点击了地图很多次,那么可能每一次都会绘制一个Marker,有时候你就不知道当前到底在哪里,那么为了解决这个问题,可以只保留一个Marker。
④ 管理Marker
首先我们声明一个变量
// 标记
private var marker: Marker? = null
然后修改changeMapCenter()
函数的代码,如下所示:
private fun changeMapCenter(latLng: LatLng) {
// 移除标点
marker?.remove()
// 添加标点
marker = map.addMarker(MarkerOptions()
.title("Marker")
.position(latLng))
// 地图中移动到经纬度处
map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM.toFloat()))
}
这里在赋值之前先移除,如果不为空就会移除再添加到,另外我们还可以在点击当前位置按钮的时候移除,代码如下所示:
setOnMyLocationButtonClickListener {
Log.d(TAG, "configMap: 点击位置图标")
// 移除标点
marker?.remove()
marker = null
return@setOnMyLocationButtonClickListener false
}
这样Marker就是唯一的一个,我们还可以修改Marker的样式。通过MarkerOptions进行设置,比如icon
(图标),alpha
(透明度)。
marker = map.addMarker(MarkerOptions()
.title("Marker") // 设置标题
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_YELLOW)) // 设置默认图标颜色为黄色
.alpha(0.7f) // 设置透明度
.position(latLng) // 设置位置
)
类似这样修改,当然icon还可以设置自定义的图标,比如这样:
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_google))
这个图标可以设置成自己需要的图标。
六、地址位置编码
地址位置编码分为两种情况,通过经纬度获取详细地址,通过地址获取经纬度坐标。无论那种方式,在国内都有限制。
① 坐标转地址
首先我们来写坐标转地址,地址的结果我们通过Address来接收,这是一个列表,首先我们声明变量:
// 地理编码器
private var geocoder: Geocoder? = null
// 地址结果
private var addressesLiveData: MutableLiveData<List<Address>> = MutableLiveData()
然后在configMap()
函数中增加如下代码:
// 初始化地理编码器
geocoder = Geocoder(this)
// 编码结果
addressesLiveData.observe(this) { addresses ->
// 获取地址信息
if (!addresses.isNullOrEmpty()) {
val address = addresses[0]
Log.d(TAG, "Address: ${address.latitude} ${address.longitude} ${address.countryName} ${address.adminArea} ${address.locality} ${address.thoroughfare} ${address.subThoroughfare}")
}
}
在观察到数据改变时,打印出来。当前的Activity需要实现接口
主要加上这个注解,然后重写onGeocode()函数,代码如下所示:
/**
* 地理编码结果,经纬度坐标转地址
*/
override fun onGeocode(addresses: MutableList<Address>) {
addressesLiveData.postValue(addresses)
}
然后再增加一个getDetailAddress()
函数
/**
* 获取详情位置信息,获取国内位置会出现异常
*/
private fun getDetailAddress(latLng: LatLng) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder?.getFromLocation(latLng.latitude, latLng.longitude, 1, this@GoogleMapActivity)
} else {
addressesLiveData.postValue(geocoder?.getFromLocation(latLng.latitude, latLng.longitude, 1))
}
}
在这里我们通过geocoder
去获取详细的地址信息,这里就需要进行版本的判断了,1表示返回的最大结果数,可以自行修改。最后在changeMapCenter()
函数中调用getDetailAddress()
函数,如下图所示:
运行后,控制台日志如下图所示:
② 地址转坐标
这里我们只需要写一个getDetailLatLng()
函数就可以了,代码如下所示:
/**
* 获取默认经纬度的地址信息
*/
private fun getDetailLatLng(address: String = "悉尼歌剧院") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder?.getFromLocationName(address, 1, this@GoogleMapActivity)
} else {
addressesLiveData.postValue(geocoder?.getFromLocationName(address, 1))
}
}
这里我们使用的是默认值悉尼歌剧院,看是否能够通过地址名称获取具体的地址信息,这里的接口是一样的,因此我们在使用的使用要么只用一个,要么通过一个变量来判断是坐标转地址还是地址转坐标。这里我只使用一个。
运行看看效果:
好的,这样就完成了,通过这个获取到的数据还不是最准确的,通过Google API接口去获取比较准备,感兴趣的可以去看看。
七、源码
因为涉及到项目,所以这里我就不贴源码,只贴上GoogleMapActivity的完整代码:
@SuppressLint("NewApi")
class GoogleMapActivity : AppCompatActivity(), OnMapReadyCallback, Geocoder.GeocodeListener {
private lateinit var binding: ActivityGoogleMapBinding
// 地图
private lateinit var map: GoogleMap
// Places API 的入口点。
private lateinit var placesClient: PlacesClient
// 融合位置信息提供程序的入口点。
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
// 最后已知位置
private var lastKnownLocation: Location? = null
// 标记
private var marker: Marker? = null
// 地理编码器
private var geocoder: Geocoder? = null
// 地址结果
private var addressesLiveData: MutableLiveData<List<Address>> = MutableLiveData()
companion object {
private val TAG = GoogleMapActivity::class.java.simpleName
// 默认缩放
private const val DEFAULT_ZOOM = 15
// 权限请求码
private const val LOCATION_PERMISSION_REQUEST_CODE = 9527
// 未授予位置权限时使用的默认位置(澳大利亚悉尼)和默认缩放。
private val defaultLocation = LatLng(-33.8523341, 151.2106085)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityGoogleMapBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initView()
}
/**
* 初始化视图
*/
private fun initView() {
// 构造 PlacesClient
Places.initialize(applicationContext, BuildConfig.MAPS_API_KEY)
placesClient = Places.createClient(this)
// 构造 FusedLocationProviderClient。
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
/**
* 检查权限
*/
private fun checkPermission() {
// 检查当前是否拥有精确位置或粗略位置权限
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
// 权限已授予,可以进行定位操作
Log.d(TAG, "checkPermission: 权限已授予")
configMap()
} else {
Log.d(TAG, "checkPermission: 请求权限")
// 请求权限
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
),
LOCATION_PERMISSION_REQUEST_CODE
)
}
}
/**
* 地图就绪
*/
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
// 检查权限
checkPermission()
}
/**
* 地图配置
*/
@SuppressLint("MissingPermission")
private fun configMap() {
Log.d(TAG, "configMap: 地图配置")
// 初始化地理编码器
geocoder = Geocoder(this)
// 编码结果
addressesLiveData.observe(this) { addresses ->
// 获取地址信息
if (!addresses.isNullOrEmpty()) {
val address = addresses[0]
Log.d(TAG, "Address: ${address.latitude} ${address.longitude} ${address.countryName} ${address.adminArea} ${address.locality} ${address.thoroughfare} ${address.subThoroughfare}")
}
}
map.apply {
isMyLocationEnabled = true // 地图上启用“我的位置”图层
// 当前位置图标的点击事件
setOnMyLocationButtonClickListener {
Log.d(TAG, "configMap: 点击位置图标")
// 移除标点
marker?.remove()
marker = null
return@setOnMyLocationButtonClickListener false
}
// 定位后的蓝点点击事件
setOnMyLocationClickListener { location ->
Log.d(TAG, "configMap: 点击我的位置 $location")
Toast.makeText(this@GoogleMapActivity, "Current location:\n$location", Toast.LENGTH_LONG).show()
}
// 地图点击事件
setOnMapClickListener { latLng ->
changeMapCenter(latLng)
}
// 地图设置
uiSettings.apply {
isZoomControlsEnabled = true // 显示缩放按钮
isMyLocationButtonEnabled = true // 显示定位按钮
isCompassEnabled = true // 显示指南针
isMapToolbarEnabled = true // 显示地图工具栏
isRotateGesturesEnabled = true // 允许旋转手势
isScrollGesturesEnabled = true // 允许滚动手势
isTiltGesturesEnabled = true // 允许倾斜手势
isZoomGesturesEnabled = true // 允许缩放手势
isScrollGesturesEnabledDuringRotateOrZoom = true // 允许在旋转或缩放时滚动手势
isIndoorLevelPickerEnabled = true // 显示室内层选择器
}
}
// 获取当前位置
getCurrentLocation()
}
/**
* 获取当前位置
*/
@SuppressLint("MissingPermission")
private fun getCurrentLocation() {
Log.d(TAG, "getCurrentLocation: 获取当前位置")
fusedLocationProviderClient.lastLocation.addOnCompleteListener { task ->
// 获取当前位置未成功
if (!task.isSuccessful) {
Log.d(TAG, "Current location is null. Using defaults.")
Log.e(TAG, "Exception: %s", task.exception)
// 设置默认位置
changeMapCenter(defaultLocation)
return@addOnCompleteListener
}
lastKnownLocation = task.result
if (lastKnownLocation == null) return@addOnCompleteListener
// 移动地图到当前位置
changeMapCenter(LatLng(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude))
}
}
/**
* 改变地图中心
*/
private fun changeMapCenter(latLng: LatLng) {
// 移除标点
marker?.remove()
// 添加标点
marker = map.addMarker(MarkerOptions()
.title("Marker") // 设置标题
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_google)) // 设置自定义图标
.alpha(0.7f) // 设置透明度
.position(latLng) // 设置位置
)
// 获取详细位置信息
// getDetailAddress(latLng)
// 获取默认经纬度的地址信息
getDetailLatLng()
// 地图中移动到经纬度处
map.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM.toFloat()))
}
/**
* 获取默认经纬度的地址信息
*/
private fun getDetailLatLng(address: String = "悉尼歌剧院") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder?.getFromLocationName(address, 1, this@GoogleMapActivity)
} else {
addressesLiveData.postValue(geocoder?.getFromLocationName(address, 1))
}
}
/**
* 获取详情位置信息,获取国内位置会出现异常
*/
private fun getDetailAddress(latLng: LatLng) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
geocoder?.getFromLocation(latLng.latitude, latLng.longitude, 1, this@GoogleMapActivity)
} else {
addressesLiveData.postValue(geocoder?.getFromLocation(latLng.latitude, latLng.longitude, 1))
}
}
/**
* 权限请求结果
*/
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
LOCATION_PERMISSION_REQUEST_CODE -> {
// 如果请求被取消,则结果数组为空
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限被授予,可以进行定位操作
Log.d(TAG, "onRequestPermissionsResult: 权限被授予")
configMap()
} else {
// 权限被拒绝,无法进行定位操作
Log.d(TAG, "onRequestPermissionsResult: 权限被拒绝")
Toast.makeText(this, "拒绝将无法使用定位功能", Toast.LENGTH_SHORT).show()
finish()
}
}
}
}
/**
* 地理编码结果,经纬度坐标转地址
*/
override fun onGeocode(addresses: MutableList<Address>) {
addressesLiveData.postValue(addresses)
}
}
该导包的地方注意导包即可。