说实话,在Android应用接入微软bing地图真的是耗费了很大的精力。市面上大部分人在Android应用也不接入bing地图,基本都是用google地图。
这或许也是bing地图对于android开发文档写的真的很烂,而且好多年没有更新过的原因吧。
在微软bing地图的Android开发文档中,提供的都是java的语法,而现在很多都是使用kotlin语法了,而且提供的案例代码都无法直接用。也没有找到太多的资料。就连官方提供的android应用的依赖包路径都不存在了......一度以为这个玩意真的没有人在Android用吗?
真的是耗费了很多时间在处理渲染地图上,接下来给大家分享如何渲染bing地图,给地图增加spin图标,绘制精度圈,绑定图标点击事件,转换经纬度的功能。
大家如果不想一步步跟着来,也可以直接跳到文末,有操作地图完整的代码内容。
当然配置地图key部分,还是得先看看第一步的内容哈~~
第一步、
由于bing地图官方文档提供的bing maps sdk的地址没法用,于是我最后在github issue github.com/microsoft/M... 中找到了一个方法解决了这个问题。
native.virtualearth.net/sdk/android... 直接下载这个包,然后放在Android项目中的libs
文件夹下,如果你的项目结构是android视图,可能看不到libs目录,可以切换到project视图,如果没有的话,那就在app下创建一个libs文件夹,把下载的包丢进去。
解决了sdk包的问题,接下来就是配置我们的地图key,如果没有的话,记得去bing地图的官网申请一下。
找到Gradle Scripts
目录,创建一个secrets.gradle
文件,内容如下:
ini
ext.credentialsKey = "填入你的key"
而后找到我们的build.gradle
(Module:app)这个文件,引入我们刚刚创建的文件 apply from: 'secrets.gradle'
,然后找到文件里的 android>buildTypes、buildFeatures
,设置以一下这个CREDENTIALS_KEY
变量,后续初始化地图的时候要用:
php
apply from: 'secrets.gradle'
android {
buildFeatures {
buildConfig = true
}
buildTypes {
buildTypes.each {
it.buildConfigField "String", "CREDENTIALS_KEY", ""$credentialsKey""
}
}
}
到这里,配置地图就完成了,接下来就是渲染地图了。
第二步、
由于我是通过Android studio通过模板创建的项目,使用的是kotlin + compose
的用法。
先找到项目的MainActivity.kt
这个文件,然后引入地图的库。
在class MainActivity
里面的顶层,定义一个全局变量存储mapView实例,方便后续的地图操作。
csharp
private var mMapView: MapView? = null // 定义 mMapView
之后找到我们的onCreate方法,使用ComposeView来显示我们的地图。
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
findViewById<ComposeView>(R.id.compose_view).setContent {
SGTTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
BingMapsView { mapView ->
mMapView = mapView // 更新 mMapView 的引用
}
// 在这里根据需要添加其他Compose UI元素
}
}
}
}
同时我们需要在MainActivity.kt文件底部,创建一个compose函数 BingMapsView :
kotlin
@Composable
fun BingMapsView(modifier: Modifier = Modifier, onMapViewCreated: (MapView) -> Unit) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { ctx ->
MapView(ctx, MapRenderMode.VECTOR).apply {
this.setCredentialsKey(BuildConfig.CREDENTIALS_KEY)
onCreate(null)
onResume()
onMapViewCreated(this) // 回调函数,返回 MapView 实例
}
},
)
}
到这里,运行一下模拟器,就可以看到成功渲染出地图了。
第三步、
给我们的地图增加spin图标,还记得我们一开始定义的mMapView地图实例吧,现在要派上用场了。比如:我们要在用户的位置上加一个图标,于是我们就定义了个函数addUserLocation
,使用MapIcon
来实例化一个图标。再加到MapElementLayer
图层上。
如果想要自定义图标的话,就是用MapImage
创建一个对象就行了。
我们把图片放在drawble
文件夹下,定义为map_icon.png
,你们随意取什么名字都行。
代码如下:
scss
private fun addUserLocation() {
mMapView?.run {
val location = Geopoint(20.12067, 113.26197)
// 加载PNG图标为Bitmap
val iconBitmap = BitmapFactory.decodeResource(resources, R.drawable.map_icon)
// 创建MapImage对象
val mapImage = MapImage(iconBitmap)
// Example action: Setting a center point and zoom level
// location 这个换成替换成用户经纬度
setScene(MapScene.createFromLocationAndZoomLevel(location, 14.0), MapAnimationKind.NONE)
// Initialize and add a MapIcon as part of map creation
val icon = MapIcon().apply {
location = location // Example location
image = mapImage
title = 'title'
}
val elementLayer = MapElementLayer().apply {
elements.add(icon)
}
layers.add(elementLayer)
}
}
这样一个用户的图标就出来了。
那如果想要在用户的位置上,再渲染一个浅蓝色的圈呢?我们该如何实现?
其实也很简单,我们创建一个函数createAccuracyCircle
,参数是经纬度,还有圆的半径多少米(我们设为500),使用这个方法MapPolygon
去绘制一个圆形出来。
kotlin
// 添加绘制精度圈
private fun createAccuracyCircle(latitude: Double,longitude: Double, radiusInMeters: Double): MapPolygon {
return MapPolygon().apply {
this.shapes = listOf(Geocircle(Geoposition(latitude, longitude), radiusInMeters))
this.fillColor = 0x669AD1F3 // 设置为半透明的蓝色
this.strokeColor = 0xFF9AD1F3.toInt() // 边缘为蓝色
this.isStrokeDashed = false
this.strokeWidth = 1
}
}
那结合上面的用户图标,addUserLocation
代码就变成这样了:
scss
private fun addUserLocation() {
mMapView?.run {
val location = Geopoint(20.12067, 113.26197)
// 加载PNG图标为Bitmap
val iconBitmap = BitmapFactory.decodeResource(resources, R.drawable.map_icon)
// 创建MapImage对象
val mapImage = MapImage(iconBitmap)
// Example action: Setting a center point and zoom level
// location 这个换成替换成用户经纬度
setScene(MapScene.createFromLocationAndZoomLevel(location, 14.0), MapAnimationKind.NONE)
// Initialize and add a MapIcon as part of map creation
val icon = MapIcon().apply {
location = location // Example location
image = mapImage
title = 'title'
}
val accuracyCircle = createAccuracyCircle(useLocation.position.latitude, useLocation.position.longitude, 500.00)
val elementLayer = MapElementLayer().apply {
elements.add(icon)
elements.add(accuracyCircle)
}
layers.add(elementLayer)
}
}
第四步、
实现给地图上的图标增加点击事件。同样的步骤,我们先写一个函数创建一个图标显示在地图上,通过遍历图层上的Icon找到我们点击的那个图片,使用elementLayer.addOnMapElementTappedListener
增加一个点击事件就好了。
实现如下:
kotlin
private fun createLocationIcon(latitude, longitude, customerName) {
mMapView?.run {
layers.clear()
val location = Geopoint(latitude, longitude)
val customIcon = MapIcon().apply {
this.location = location
this.title = customerName
}
val elementLayer = MapElementLayer().apply {
elements.add(customIcon)
}
layers.add(elementLayer)
setScene(MapScene.createFromLocationAndZoomLevel(location, 14.0), MapAnimationKind.NONE)
elementLayer.addOnMapElementTappedListener { mapArgs ->
for (mapElement in mapArgs.mapElements) {
if (mapElement is MapIcon) {
// 检查是否是我们感兴趣的MapIcon
if (mapElement.title == customerName) {
// 在这里处理点击事件,
break // 如果找到了,就没有继续遍历的必要
}
}
}
true // 表示事件已处理
}
}
}
第五步、
地图上几个图标,我们怎么判断图标在不在刚刚我们绘制的圆圈范围内?这里直接给出两个函数,需要传圆圈中心点的经纬度(我们刚刚的用户位置),跟要判断是否在圈内的经纬度,大家直接用就好了。
kotlin
// 计算两点之间的距离
private fun distanceBetween(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
val earthRadius = 6371000.0 // 地球半径,单位:米
val dLat = Math.toRadians(lat2 - lat1)
val dLon = Math.toRadians(lon2 - lon1)
val a = sin(dLat / 2) * sin(dLat / 2) +
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
sin(dLon / 2) * sin(dLon / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return earthRadius * c
}
// 判断用户位置是否在精度圈内
private fun isUserInAccuracyCircle(userLocation: Geopoint, centerLatitude: Double, centerLongitude: Double, radiusInMeters: Double): Boolean {
val distance = distanceBetween(userLocation.position.latitude, userLocation.position.longitude, centerLatitude, centerLongitude)
return distance <= radiusInMeters
}
第六步、
由于使用的是基于WGS-84坐标系统的国际地图服务(微软bing maps sdk),如果需要在国内展示位置数据,那么就需要将这些WGS-84坐标转换为GCJ-02坐标,这是因为中国的法律规定,在中国发布的地图服务必须使用GCJ-02坐标系统,而直接使用WGS-84坐标系统在中国境内可能会导致地理位置显示不准确。
于是我们在获取用户经纬度之后(大家不知道怎么获取用户位置经纬度的,可以看我上一篇文章),需要先转换成GCJ-02坐标,再设置到地图上。
转换的方法我们定义在一个对象Gcj02Converter
中,代码如下:
kotlin
object Gcj02Converter {
private const val a = 6378245.0
private const val ee = 0.00669342162296594323
private fun transformLat(x: Double, y: Double): Double {
var lat = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(abs(x))
lat += (20.0 * sin(6.0 * x * Math.PI) + 20.0 * sin(2.0 * x * Math.PI)) * 2.0 / 3.0
lat += (20.0 * sin(y * Math.PI) + 40.0 * sin(y / 3.0 * Math.PI)) * 2.0 / 3.0
lat += (160.0 * sin(y / 12.0 * Math.PI) + 320 * sin(y * Math.PI / 30.0)) * 2.0 / 3.0
return lat
}
private fun transformLon(x: Double, y: Double): Double {
var lon = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(abs(x))
lon += (20.0 * sin(6.0 * x * Math.PI) + 20.0 * sin(2.0 * x * Math.PI)) * 2.0 / 3.0
lon += (20.0 * sin(x * Math.PI) + 40.0 * sin(x / 3.0 * Math.PI)) * 2.0 / 3.0
lon += (150.0 * sin(x / 12.0 * Math.PI) + 300.0 * sin(x / 30.0 * Math.PI)) * 2.0 / 3.0
return lon
}
fun wgs84ToGcj02(wgsLat: Double, wgsLon: Double): Geopoint {
// 判断是否在国内
if (outOfChina(wgsLat, wgsLon)) {
return Geopoint(wgsLat, wgsLon)
}
var dLat = transformLat(wgsLon - 105.0, wgsLat - 35.0)
var dLon = transformLon(wgsLon - 105.0, wgsLat - 35.0)
val radLat = wgsLat / 180.0 * Math.PI
var magic = sin(radLat)
magic = 1 - ee * magic * magic
val sqrtMagic = sqrt(magic)
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI)
dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * Math.PI)
val mgLat = wgsLat + dLat
val mgLon = wgsLon + dLon
return Geopoint(mgLat, mgLon)
}
private fun outOfChina(lat: Double, lon: Double): Boolean {
if (lon < 72.004 || lon > 137.8347) return true
return lat < 0.8293 || lat > 55.8271
}
}
使用的时候,直接调用里面的wgs84ToGcj02
方法就好了。
比如:
ini
val location = Gcj02Converter.wgs84ToGcj02(latitude, longitude)
转换经纬度实例,我一起放在下面的完整内容中,大家可以看完整的。
到这里,基本上今天要分享的地图功能就完成了。
最后,上述MainActivity.kt完整文件如下(import 进来多余的库,大家可以删掉):
kotlin
package com.example.sgt
import android.Manifest
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.example.sgt.ui.theme.SGTTheme
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.microsoft.maps.Geocircle
import com.microsoft.maps.Geopoint
import com.microsoft.maps.Geoposition
import com.microsoft.maps.MapAnimationKind
import com.microsoft.maps.MapElementLayer
import com.microsoft.maps.MapIcon
import com.microsoft.maps.MapImage
import com.microsoft.maps.MapPolygon
import com.microsoft.maps.MapRenderMode
import com.microsoft.maps.MapScene
import com.microsoft.maps.MapView
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
class MainActivity : ComponentActivity() {
private var mMapView: MapView? = null // 定义 mMapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
findViewById<ComposeView>(R.id.compose_view).setContent {
SGTTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
BingMapsView { mapView ->
mMapView = mapView // 更新 mMapView 的引用
}
// 在这里根据需要添加其他Compose UI元素
}
}
}
}
private fun createLocationIcon(latitude, longitude, customerName) {
mMapView?.run {
layers.clear()
// 转换经纬度实例
val gpsGcj02 = Gcj02Converter.wgs84ToGcj02(latitude, longitude)
val location = gpsGcj02
val customIcon = MapIcon().apply {
this.location = location
this.title = customerName
}
val elementLayer = MapElementLayer().apply {
elements.add(customIcon)
}
layers.add(elementLayer)
setScene(MapScene.createFromLocationAndZoomLevel(location, 14.0), MapAnimationKind.NONE)
elementLayer.addOnMapElementTappedListener { mapArgs ->
for (mapElement in mapArgs.mapElements) {
if (mapElement is MapIcon) {
// 检查是否是我们感兴趣的MapIcon
if (mapElement.title == customerName) {
// 在这里处理点击事件,
break // 如果找到了,就没有继续遍历的必要
}
}
}
true // 表示事件已处理
}
}
}
private fun addUserLocation() {
mMapView?.run {
// Actions to perform when the MapView is available
// 加载PNG图标为Bitmap
val iconBitmap = BitmapFactory.decodeResource(resources, R.drawable.map_icon)
// 创建MapImage对象
val mapImage = MapImage(iconBitmap)
// Example action: Setting a center point and zoom level
// Ensure this is only done once or controlled as needed
setScene(MapScene.createFromLocationAndZoomLevel(useLocation, 14.0), MapAnimationKind.NONE)
// Initialize and add a MapIcon as part of map creation
val icon = MapIcon().apply {
location = useLocation // Example location
image = mapImage
}
val accuracyCircle = createAccuracyCircle(useLocation.position.latitude, useLocation.position.longitude, 500.00)
val elementLayer = MapElementLayer().apply {
elements.add(icon)
elements.add(accuracyCircle)
}
layers.add(elementLayer)
}
}
// 添加绘制精度圈
private fun createAccuracyCircle(latitude: Double,longitude: Double, radiusInMeters: Double): MapPolygon {
return MapPolygon().apply {
this.shapes = listOf(Geocircle(Geoposition(latitude, longitude), radiusInMeters))
this.fillColor = 0x669AD1F3 // 设置为半透明的蓝色
this.strokeColor = 0xFF9AD1F3.toInt() // 边缘为蓝色
this.isStrokeDashed = false
this.strokeWidth = 1
}
}
// 计算两点之间的距离
private fun distanceBetween(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
val earthRadius = 6371000.0 // 地球半径,单位:米
val dLat = Math.toRadians(lat2 - lat1)
val dLon = Math.toRadians(lon2 - lon1)
val a = sin(dLat / 2) * sin(dLat / 2) +
cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) *
sin(dLon / 2) * sin(dLon / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return earthRadius * c
}
// 判断用户位置是否在精度圈内
private fun isUserInAccuracyCircle(userLocation: Geopoint, centerLatitude: Double, centerLongitude: Double, radiusInMeters: Double): Boolean {
val distance = distanceBetween(userLocation.position.latitude, userLocation.position.longitude, centerLatitude, centerLongitude)
return distance <= radiusInMeters
}
}
object Gcj02Converter {
private const val a = 6378245.0
private const val ee = 0.00669342162296594323
private fun transformLat(x: Double, y: Double): Double {
var lat = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(abs(x))
lat += (20.0 * sin(6.0 * x * Math.PI) + 20.0 * sin(2.0 * x * Math.PI)) * 2.0 / 3.0
lat += (20.0 * sin(y * Math.PI) + 40.0 * sin(y / 3.0 * Math.PI)) * 2.0 / 3.0
lat += (160.0 * sin(y / 12.0 * Math.PI) + 320 * sin(y * Math.PI / 30.0)) * 2.0 / 3.0
return lat
}
private fun transformLon(x: Double, y: Double): Double {
var lon = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(abs(x))
lon += (20.0 * sin(6.0 * x * Math.PI) + 20.0 * sin(2.0 * x * Math.PI)) * 2.0 / 3.0
lon += (20.0 * sin(x * Math.PI) + 40.0 * sin(x / 3.0 * Math.PI)) * 2.0 / 3.0
lon += (150.0 * sin(x / 12.0 * Math.PI) + 300.0 * sin(x / 30.0 * Math.PI)) * 2.0 / 3.0
return lon
}
fun wgs84ToGcj02(wgsLat: Double, wgsLon: Double): Geopoint {
// 判断是否在国内
if (outOfChina(wgsLat, wgsLon)) {
return Geopoint(wgsLat, wgsLon)
}
var dLat = transformLat(wgsLon - 105.0, wgsLat - 35.0)
var dLon = transformLon(wgsLon - 105.0, wgsLat - 35.0)
val radLat = wgsLat / 180.0 * Math.PI
var magic = sin(radLat)
magic = 1 - ee * magic * magic
val sqrtMagic = sqrt(magic)
dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI)
dLon = (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * Math.PI)
val mgLat = wgsLat + dLat
val mgLon = wgsLon + dLon
return Geopoint(mgLat, mgLon)
}
private fun outOfChina(lat: Double, lon: Double): Boolean {
if (lon < 72.004 || lon > 137.8347) return true
return lat < 0.8293 || lat > 55.8271
}
}
@Composable
fun BingMapsView(modifier: Modifier = Modifier, onMapViewCreated: (MapView) -> Unit) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { ctx ->
MapView(ctx, MapRenderMode.VECTOR).apply {
this.setCredentialsKey(BuildConfig.CREDENTIALS_KEY)
onCreate(null)
onResume()
onMapViewCreated(this) // 回调函数,返回 MapView 实例
}
},
)
}
OK,如果大家对上面分享的内容,还有疑问的话,欢迎评论区留言!