Android Google Maps

news2025/3/12 21:33:08

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.gradlebuildFeatures 部分中 或 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文件中,设置 targetSdkcompileSdk 到 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可以设置的,同样可以通过代码设置。就需要用到GoogleMapOptionsUiSettings,如果你使用的是动态加载的地图,那么就使用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)
    }
}

该导包的地方注意导包即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2102065.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

达梦SQL 优化简介

目录 一、定位慢 SQL &#xff08;一&#xff09;开启跟踪日志记录 1.跟踪日志记录配置 &#xff08;二&#xff09;通过系统视图查看 1.SQL 记录配置 2.查询方式 二、SQL分析方法 &#xff08;一&#xff09;执行计划 1.概述 2.查看执行计划 &#xff08;二&#x…

使用LLaMA-Factory快速训练自己的专用大模型

本文聊聊 LLama-Factory&#xff0c;它是一个开源框架&#xff0c;这里头可以找到一系列预制的组件和模板&#xff0c;让你不用从零开始&#xff0c;就能训练出自己的语言模型&#xff08;微调&#xff09;。不管是聊天机器人&#xff0c;还是文章生成器&#xff0c;甚至是问答…

物联网能源管理平台

能源管理平台可以对接各种物联网设备&#xff0c;感兴趣的朋友可根据下方方式联系获取演示账号 1.介绍 2.智慧照明接入 设备远程控制&#xff0c;灯光亮度感应等模式设置&#xff0c;用电耗能分析 3.温控器接入 温控器&#xff08;空调面板&#xff09;接入&#xff0c;实现…

GB/T28181规范和JT1078交通部标差异

技术背景 好多开发者区分不太清楚GB/T28181和JT1078规范&#xff0c;实际上&#xff0c;二者在规范定义、技术特点、过检认证以及应用场景等方面均存在显著差异。两者各有其适用领域和优势&#xff0c;但在某些特定场景下也可能需要相互协作以实现更全面的监控和管理。 规范定…

C#复习之封装_静态成员

知识点一 静态成员的基本概念 知识点二 早已出现的静态成员 知识点四 静态成员的使用 知识点五 为什么可以直接点出来 //记住&#xff01; //程序中是不能无中生有的 //我们要使用的对象&#xff0c;变量&#xff0c;函数都是要在内存中分配内存空间的 //之所以要实例化对象…

TWS蓝牙耳机发展历史以及涉及的相关技术知识

TWS(True Wireless Stereo)蓝牙耳机是近年来迅速发展的消费电子产品之一。它们的诞生和发展经历了多个阶段,并涉及到多项关键技术。以下是 TWS 蓝牙耳机的发展历史及相关技术知识的详细解析。 1. TWS 蓝牙耳机的发展历史 1.1 初期阶段(2014年之前) 在 TWS 蓝牙耳机出现之…

老师怎样用微信发布月考成绩?

每当月考结束&#xff0c;老师们就开始了一项繁琐的任务——将成绩单一一私信给每位学生的家长。这不仅耗时耗力&#xff0c;还容易出错。家长们焦急等待&#xff0c;老师们则在电脑前忙碌着&#xff0c;一张张成绩单&#xff0c;一条条信息&#xff0c;重复的工作让人疲惫不堪…

Unity(2022.3.41LTS) - UI详细介绍-Scrollbar(滚动条)

目录 零.简介 一、基本功能与用途 二、组件介绍 三、使用方法 四、优化和注意事项 五.和滑动条的区别 零.简介 在 Unity 中&#xff0c;滚动条&#xff08;Scrollbar&#xff09;是一种用于实现滚动功能的 UI 组件。 一、基本功能与用途 滚动内容&#xff1a;主要用于…

【重学 MySQL】五、MySQL 的卸载

【重学 MySQL】五、MySQL 的卸载 停止MySQL服务卸载MySQL程序删除残余文件清理注册表删除环境变量配置重启电脑 MySQL的卸载过程需要仔细操作&#xff0c;以确保彻底卸载并清理所有相关文件和配置。 停止MySQL服务 打开任务管理器&#xff1a;右键点击任务栏空白处&#xff0…

模组级存储与嵌入式存储的千差万别

雷龙在多年销售CS创世 SD NAND&#xff08;也称&#xff1a;迷你型eMMC&#xff0c;小尺寸/小容量eMMC&#xff0c;贴片式T卡&#xff0c;贴片式TF卡&#xff0c;贴片式SD卡等&#xff09;的过程中&#xff0c;经常有被用户问到&#xff1a;SD NAND 与 TF卡 到底有哪些区别呢&a…

动态路由和路由导航守卫及其案例分析

为什么需要动态路由&#xff1f; 动态路由其实用的不多&#xff0c;在实际开发中&#xff0c;如果遇到权限分配问题&#xff0c;比如对于一个公司人员的后台管理系统&#xff0c;那对不同成员的权限肯定不同&#xff0c;对于人事部&#xff0c;他们有权限进入成员表对人员的流…

【网络安全】XSS漏洞挖掘Tip

未经许可&#xff0c;不得转载。 某网页存在错误信息&#xff0c;包含在div标签内&#xff1a; 检查此页面的源代码以获取id属性&#xff1a; 将id属性作为 GET 参数进行测试&#xff0c;例如在 URL 中添加error_messagehelloworld&#xff1a; 可以看到&#xff0c;参数被接受…

流媒体技术革新,EasyCVR视频汇聚平台赋能视频监控全面升级

随着科技的飞速发展&#xff0c;流媒体技术和视频监控正经历着前所未有的变革与融合。本文将从流媒体技术的新兴趋势出发&#xff0c;探讨其与视频监控领域的深度结合&#xff0c;以及这一融合所带来的创新与发展。 一、流媒体技术的新兴趋势 1、5G网络的广泛应用 5G网络以其…

AI驱动测试管理工具会有哪些发展前景呢?

在软件测试领域&#xff0c;人工智能&#xff08;AI&#xff09;的出现犹如一场技术革命&#xff0c;改变了传统的测试管理方式。随着AI技术的迅速发展&#xff0c;它将如何进一步提升测试管理的效率与准确性&#xff1f;未来的AI驱动测试管理工具又会带来哪些令人期待的创新呢…

unittest | 使用unittest模块来测试logging日志模块功能

我们在这篇文章实现了在项目工程中编写一个logging模块&#xff0c;但是我们如何确定我们编写的模块功能的是否正常? 你可能想到将全部代码写完后运行测试&#xff0c;但这是一个非常不好的习惯。❌ 最好的方式&#xff0c;是每写出来一个功能或者方法就对它进行测试&#x…

MySQL 存储引擎有哪些?InnoDB 和 MyISAM 存储引擎有什么区别?

MySQL 存储引擎有哪些&#xff1f; MySQL 存储引擎主要负责查询的执行和数据的存储&#xff0c;存储引擎主要有 InnoDB&#xff0c;MyISAM&#xff0c;Memery InnoDB 是 MySQL 默认的存储引擎&#xff0c;支持事务和行级锁&#xff0c;以及外键的约束&#xff0c;具有事务提交…

揭秘推荐算法:深度学习如何读懂你的购物心思

时间&#xff1a;2024年09月03日 作者&#xff1a;小蒋聊技术 邮箱&#xff1a;wei_wei10163.com 微信&#xff1a;wei_wei10 音频&#xff1a;https://xima.tv/1_L8HH40?_sonic0 希望大家帮个忙&#xff01;如果大家有工作机会&#xff0c;希望帮小蒋内推一下&#xff0c…

帅地:秋招入职腾讯,后裸辞创业年入百万,一位全职程序员博主的第六年

这是《开发者说》的第17期&#xff0c;这次我们采访的是知名的程序员博主&#xff1a;帅地。 帅地从大学开始接触公众号&#xff0c;在大学毕业时就沉淀了10w垂直粉丝&#xff0c;月入近10w。在秋招阶段&#xff0c;帅地靠扎实的算法基础和技术功底顺利入职腾讯。“还没折腾够”…

【分布式注册中心】NACOS_2.3.0部署与实战

部署 一 准备 1 依赖&#xff1a;MYSQL 2 创建数据库 CREATE database if NOT EXISTS nacos default character set utf8mb4 collate utf8mb4_unicode_ci; 3 导入初始化SQL https://raw.githubusercontent.com/alibaba/nacos/develop/distribution/conf/mysql-schema.sql…

了解Spring Data JPA

1、Spring Data JPA 1.1、概述 Spring Data JPA 是 Spring 基于JPA 规范的基础上封装的⼀套 JPA 应⽤框架&#xff0c;可使开发者⽤极简的代码即可实现对数据库的访问和操作。它提供了包括增删改查等在内的常⽤功能&#xff01;学习并使⽤Spring Data JPA 可以极⼤提⾼开发效率…