上网查了一些资料发现有关谷歌地图sdk集成的文章还是不少的,但是都缺乏系统性。这里做些系统整理,主要分以下篇幅讲解,希望对初始谷歌地图的你有所帮助:
- 【谷歌地图–集成准备】
- 【谷歌地图–MapsSDK集成】
- 【谷歌地图–DirectionsSDK集成】
- 【谷歌地图–PlacesSDK集成】
开始正文啦:
由于众所周知的的原因,集成谷歌地图sdk前首先你的pc端和移动端都是要翻墙的,不然后续的一些功能你都无法操作。
谷歌地图–DirectionsSDK集成
开始正文之前,我们这里先做个说明哈:集成 Directions 功能时,需要你的google地图开发者账号启用计费功能。而启用计费功能,就需要绑定一个国际的信用卡。什么!国际的信用卡,我没有啊,只有国内的行不行,答案是不行滴。顿时心中一万个不爽,集成国内的地图,就没有这么麻烦!不过大家也不要担心,无法启用计费功能的同学,后面我会为大家提供解决办法,走起!
截图:
线路规划
同样废话不多说,先上效果图:
首先明确一点,获取这些路线信息,不像集成国内的地图。谷歌地图而是一个http get请求。链接如下:
https://maps.googleapis.com/maps/api/directions/json?origin=Toronto&destination=Montreal&avoid=highways&mode=bicycling&key=your_api_kay
具体传递参数的意义大家可仔细阅读官方文档,这里我也贴出关键代码:
/**
* 获取线路信息
*
* origin,destination :可是以下几种:位置ID,地址,字符串经纬度
*
* mode: driving (默认)表示使用道路网络的标准行车路线。
* walking
* bicycling
* transit 通过公共交通路线(如果有)请求路线。如果将模式设置为transit,则可以选择指定departure_time或 arrival_time。
* 如果未指定任何时间,则 departure_time默认为现在(即,出发时间默认为当前时间)。
* 您还可以选择包含 transit_mode和和/或 transit_routing_preference。
*
* arrival_time:指定公交路线的理想到达时间
* departure_time:指定所需的出发时间。
*
* waypoints:指定要在起点和终点之间的路径上包括的直通或中途停留位置的中间位置数组。航点通过将路线引导通过指定位置来更改路线
* API支持以下行驶模式的航点:驾驶,步行和骑车;不中转。
*
* alternatives:如果设置为 true,则指定“方向”服务可以在响应中提供多个路线选择。
* 请注意,提供路由选择可能会增加服务器的响应时间。这仅适用于没有中间航路点的请求。
*
* avoid: 表示计算出的路线应避开所指示的特征。highways 表示计算出的路线应避开高速公路。
*
* units:指定显示结果时要使用的单位制。有效值在下面的“单位系统”中指定 。
*
*/
private fun getLineInfo() {
ViseHttp.GET("directions/json")
.tag("tag1")
.addParam("origin", strOrigin)
.addParam("destination", strDestination)
//.addParam("mode", "driving")
.addParam(
"waypoints",
"$strWaypoints2"
) //航路点,最大允许数量 waypoints为25,加上起点和终点 $strWaypoints1|$strWaypoints2
//.addParam("alternatives", "true") //如果设置为 true,这仅适用于没有中间航路点的请求。
//.addParam("avoid", "tolls|highways|ferries")
.addParam("key", Config.API_KAY)
.request(object : ACallback<LineModule?>() {
override fun onSuccess(mBean: LineModule?) {
//请求成功,AuthorModel为解析服务器返回数据的对象,可以是String,按需定义即可
dealWithData(mBean)
}
override fun onFail(errCode: Int, errMsg: String) {
//请求失败,errCode为错误码,errMsg为错误描述
Log.i(TAG, " errCode=${errCode} errMsg=$errMsg")
}
})
}
那我们获取的数据是什么样的呢?这里,我把完整的json数据贴到这里,供大家使用。不方便启用计费功能的同学,可以完整的复制该json数据到自己的项目中,以读取本地json数据的方式,模拟数据请求,然后解析并做后面的展示处理。每个返回参数的意义,同样参考官方文档 。
json数据:
{
"geocoded_waypoints" : [
{
"geocoder_status" : "OK",
"place_id" : "ChIJy-z5i18alTURP4V2gaGOgZY",
"types" : [ "establishment", "point_of_interest", "transit_station" ]
},
{
"geocoder_status" : "OK",
"place_id" : "ChIJyXsMpF0alTURAOY_NJyY1U8",
"types" : [ "establishment", "point_of_interest" ]
}
],
"routes" : [
{
"bounds" : {
"northeast" : {
"lat" : 36.7065579,
"lng" : 119.1854509
},
"southwest" : {
"lat" : 36.7023932,
"lng" : 119.1759237
}
},
"copyrights" : "Map data ©2020",
"legs" : [
{
"distance" : {
"text" : "1.5 公里",
"value" : 1515
},
"duration" : {
"text" : "4分钟",
"value" : 241
},
"end_address" : "中国山东省潍坊市奎文区胜利东街华都鲁成大厦",
"end_location" : {
"lat" : 36.7065579,
"lng" : 119.1772408
},
"start_address" : "中国潍坊市奎文区健康街潍县中路口",
"start_location" : {
"lat" : 36.7045897,
"lng" : 119.1854509
},
"steps" : [
{
"distance" : {
"text" : "37 米",
"value" : 37
},
"duration" : {
"text" : "1分钟",
"value" : 7
},
"end_location" : {
"lat" : 36.7047135,
"lng" : 119.1851888
},
"html_instructions" : "向\u003cb\u003e北\u003c/b\u003e行驶",
"polyline" : {
"points" : "uz__FalmvUMBSHDVBL"
},
"start_location" : {
"lat" : 36.7045897,
"lng" : 119.1854509
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "52 米",
"value" : 52
},
"duration" : {
"text" : "1分钟",
"value" : 15
},
"end_location" : {
"lat" : 36.70426399999999,
"lng" : 119.1853314
},
"html_instructions" : "向\u003cb\u003e左\u003c/b\u003e转,前往\u003cb\u003e804省道\u003c/b\u003e/\u003cwbr/\u003e\u003cb\u003e健康东街\u003c/b\u003e",
"maneuver" : "turn-left",
"polyline" : {
"points" : "m{__FmjmvUxA["
},
"start_location" : {
"lat" : 36.7047135,
"lng" : 119.1851888
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.8 公里",
"value" : 850
},
"duration" : {
"text" : "2分钟",
"value" : 127
},
"end_location" : {
"lat" : 36.7023932,
"lng" : 119.1760839
},
"html_instructions" : "向\u003cb\u003e右\u003c/b\u003e转,进入\u003cb\u003e804省道\u003c/b\u003e/\u003cwbr/\u003e\u003cb\u003e健康东街\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e继续沿健康东街前行\u003c/div\u003e",
"maneuver" : "turn-right",
"polyline" : {
"points" : "sx__FikmvULlAVtBHp@VtB@NTlBJv@fCzSn@lFf@`ERbBTfB"
},
"start_location" : {
"lat" : 36.70426399999999,
"lng" : 119.1853314
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.5 公里",
"value" : 458
},
"duration" : {
"text" : "1分钟",
"value" : 64
},
"end_location" : {
"lat" : 36.7065083,
"lng" : 119.1759237
},
"html_instructions" : "向\u003cb\u003e右\u003c/b\u003e转,进入\u003cb\u003e金马路\u003c/b\u003e",
"maneuver" : "turn-right",
"polyline" : {
"points" : "}l__FoqkvUoA@[@sHJ}ABoCBgABc@@}@@"
},
"start_location" : {
"lat" : 36.7023932,
"lng" : 119.1760839
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.1 公里",
"value" : 118
},
"duration" : {
"text" : "1分钟",
"value" : 28
},
"end_location" : {
"lat" : 36.7065579,
"lng" : 119.1772408
},
"html_instructions" : "向\u003cb\u003e右\u003c/b\u003e转,进入\u003cb\u003e胜利东街\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e目的地在左侧\u003c/div\u003e",
"maneuver" : "turn-right",
"polyline" : {
"points" : "uf`_FopkvUCgBEsC?K"
},
"start_location" : {
"lat" : 36.7065083,
"lng" : 119.1759237
},
"travel_mode" : "DRIVING"
}
],
"traffic_speed_entry" : [],
"via_waypoint" : []
}
],
"overview_polyline" : {
"points" : "uz__FalmvUa@LHd@xA[LlA`@fDz@jHrFnd@TfBoA@oILyIL}@@CgBE_D"
},
"summary" : "健康东街和金马路",
"warnings" : [],
"waypoint_order" : []
},
{
"bounds" : {
"northeast" : {
"lat" : 36.7069392,
"lng" : 119.1854509
},
"southwest" : {
"lat" : 36.7039003,
"lng" : 119.1764395
}
},
"copyrights" : "Map data ©2020",
"legs" : [
{
"distance" : {
"text" : "1.4 公里",
"value" : 1357
},
"duration" : {
"text" : "5分钟",
"value" : 279
},
"end_address" : "中国山东省潍坊市奎文区胜利东街华都鲁成大厦",
"end_location" : {
"lat" : 36.7065579,
"lng" : 119.1772408
},
"start_address" : "中国潍坊市奎文区健康街潍县中路口",
"start_location" : {
"lat" : 36.7045897,
"lng" : 119.1854509
},
"steps" : [
{
"distance" : {
"text" : "37 米",
"value" : 37
},
"duration" : {
"text" : "1分钟",
"value" : 7
},
"end_location" : {
"lat" : 36.7047135,
"lng" : 119.1851888
},
"html_instructions" : "向\u003cb\u003e北\u003c/b\u003e行驶",
"polyline" : {
"points" : "uz__FalmvUMBSHDVBL"
},
"start_location" : {
"lat" : 36.7045897,
"lng" : 119.1854509
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "52 米",
"value" : 52
},
"duration" : {
"text" : "1分钟",
"value" : 15
},
"end_location" : {
"lat" : 36.70426399999999,
"lng" : 119.1853314
},
"html_instructions" : "向\u003cb\u003e左\u003c/b\u003e转,前往\u003cb\u003e804省道\u003c/b\u003e/\u003cwbr/\u003e\u003cb\u003e健康东街\u003c/b\u003e",
"maneuver" : "turn-left",
"polyline" : {
"points" : "m{__FmjmvUxA["
},
"start_location" : {
"lat" : 36.7047135,
"lng" : 119.1851888
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.2 公里",
"value" : 167
},
"duration" : {
"text" : "1分钟",
"value" : 41
},
"end_location" : {
"lat" : 36.7039003,
"lng" : 119.1835148
},
"html_instructions" : "向\u003cb\u003e右\u003c/b\u003e转,进入\u003cb\u003e804省道\u003c/b\u003e/\u003cwbr/\u003e\u003cb\u003e健康东街\u003c/b\u003e",
"maneuver" : "turn-right",
"polyline" : {
"points" : "sx__FikmvULlAVtBHp@VtB"
},
"start_location" : {
"lat" : 36.70426399999999,
"lng" : 119.1853314
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.3 公里",
"value" : 340
},
"duration" : {
"text" : "1分钟",
"value" : 52
},
"end_location" : {
"lat" : 36.7069392,
"lng" : 119.1839711
},
"html_instructions" : "向\u003cb\u003e右\u003c/b\u003e转,进入\u003cb\u003e222省道\u003c/b\u003e/\u003cwbr/\u003e\u003cb\u003e潍县中路\u003c/b\u003e",
"maneuver" : "turn-right",
"polyline" : {
"points" : "kv__F}_mvUgBOYCoBQkBMSAwAMa@E_AIOAEA]C"
},
"start_location" : {
"lat" : 36.7039003,
"lng" : 119.1835148
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "0.7 公里",
"value" : 672
},
"duration" : {
"text" : "2分钟",
"value" : 118
},
"end_location" : {
"lat" : 36.70669,
"lng" : 119.1764407
},
"html_instructions" : "向\u003cb\u003e左\u003c/b\u003e转,进入\u003cb\u003e胜利东街\u003c/b\u003e",
"maneuver" : "turn-left",
"polyline" : {
"points" : "ki`_FybmvU?PFrD@|AB|AFjGBpBDxDFbFD|DBdC"
},
"start_location" : {
"lat" : 36.7069392,
"lng" : 119.1839711
},
"travel_mode" : "DRIVING"
},
{
"distance" : {
"text" : "89 米",
"value" : 89
},
"duration" : {
"text" : "1分钟",
"value" : 46
},
"end_location" : {
"lat" : 36.7065579,
"lng" : 119.1772408
},
"html_instructions" : "\u003cb\u003e调头\u003c/b\u003e\u003cdiv style=\"font-size:0.9em\"\u003e目的地在左侧\u003c/div\u003e",
"maneuver" : "uturn-left",
"polyline" : {
"points" : "yg`_FwskvU^?EsC?K"
},
"start_location" : {
"lat" : 36.70669,
"lng" : 119.1764407
},
"travel_mode" : "DRIVING"
}
],
"traffic_speed_entry" : [],
"via_waypoint" : []
}
],
"overview_polyline" : {
"points" : "uz__FalmvUa@LHd@xA[LlA`@fDVtBgBOiCUyFc@sBQL`KXzWHbI^?EsC?K"
},
"summary" : "222省道/潍县中路和胜利东街",
"warnings" : [],
"waypoint_order" : []
}
],
"status" : "OK"
}
解析并处理的关键代码:
private fun dealWithData(mBean: LineModule?) {
val status = mBean!!.status
if (status == "OK") {
//包含一个数组,其中包含有关起点,目的地和航点的地理编码的详细信息。
val geocodedWaypoints = mBean!!.geocoded_waypoints
Log.i(TAG, "geocodedWaypoints.size=${geocodedWaypoints.size}")
val routes = mBean!!.routes //路线 测试发现:第一条数据就是最优的线路
if (routes != null && routes.size > 0) {
Log.i(TAG, "routes.size=${routes.size}")
var routesNumber = 0;
routes.forEach {
routesNumber++;
//包含的视口边界框 overview_polyline。
val bounds = it.bounds
routesGlobalPreview(bounds);
//包含一个points 对象,该对象保存路线的编码折线表示。该折线是结果方向的近似(平滑)路径。
val overviewPolyline = it.overview_polyline
val points = overviewPolyline.points
Log.i(TAG, "points=$points")
// val mLatLngList: MutableList<LatLng> = decodePoly(points) as MutableList<LatLng>
val mLatLngList: MutableList<LatLng> = PolyUtil.decode(points)
Log.i(TAG, " mLatLngList.size=${mLatLngList.size}")
showLine(mLatLngList, routesNumber);
//包含一个数组,该数组包含有关给定路线内两个位置之间路线段的信息。对于指定的每个航点或目的地,将显示一条单独的航段。
// (没有航路点的路线将在legs阵列中仅包含一条腿。)每条腿由一系列组成steps。(请参见下面的“ 指导腿”。)
val legs = it.legs
Log.i(TAG, "legs.size=${legs.size}")
//总时间显示处理
calculateTotalTime(legs)
// 包含该路线的简短文字说明,适用于对路线进行命名和消除歧义。
val summary = it.summary
//包含显示这些方向时要显示的警告数组。您必须自己处理并显示这些警告。
val warnings = it.warnings
//包含要为此路线显示的版权文本。您必须自己处理和显示此信息。
val copyrights = it.copyrights
//包含一个数组,该数组指示计算出的路线中任何路标的顺序。如果请求是optimize:true在其waypoints参数内 传递的,则此路点可能会重新排序。
val waypointOrder = it.waypoint_order
}
} else {
Log.i(TAG, " routes is null")
}
} else {
if (status == "ZERO_RESULTS") {
Log.i(TAG, " status 在起点和终点之间找不到路由")
} else {
Log.i(TAG, " status=${status}")
}
}
}
实现导航功能
我们在 谷歌地图–MapsSDK集成 这篇文章末尾,留了这2个疑问:
- 截图中定位蓝点图标我想换成自己的怎么办呢?
- 换成自己的图标后,蓝点的方向变化又要如何处理?
那么在这里就做出一下回答。
替换蓝点
关键代码:
private fun getDeviceLocation() {
val selfPermission4 =
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
if (selfPermission4 != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
),
12
)
return
} else {
Log.i(TAG, "getDeviceLocation: 已有权限")
}
//就可以去掉官方自带的定位按钮。
mGoogleMap.isMyLocationEnabled = false
mGoogleMap.uiSettings?.isMyLocationButtonEnabled = true
//这行代码,就能发起定位请求
val locationResult = fusedLocationProviderClient.lastLocation
locationResult.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
// Set the map's camera position to the current location of the device.
val lastKnownLocation = task.result
if (lastKnownLocation != null) {
Log.i(TAG, "getDeviceLocation: locationResult if")
val currentLocation =
LatLng(lastKnownLocation!!.latitude, lastKnownLocation!!.longitude)
mGoogleMap.animateCamera(
CameraUpdateFactory.newLatLngZoom(
currentLocation,
17.3f
)
)
if (isFirstLocation) {
val fbearin = lastKnownLocation.bearing //和道路一个方向
println("MainActivity.getDeviceLocation fbearin=" + fbearin)
//添加自定义定位蓝点
mPositionMarker = mGoogleMap!!.addMarker(
MarkerOptions()
.position(currentLocation)
.title("我的位置")
.rotation(fbearin)
.anchor(0.5f, 0.5f)
.icon(BitmapDescriptorFactory.fromResource(R.drawable.baseline_arrow_circle_up_red_900_24dp))
)
}
mPositionMarker!!.position = currentLocation;
//添加自定义蓝点范围
val circle: Circle = mGoogleMap!!.addCircle(
CircleOptions().apply {
center(currentLocation)
if (isFirstLocation) {
radius(100.00)
strokeWidth(3f)
strokeColor(
ContextCompat.getColor(
this@MainActivity,
R.color.purple
)
)
fillColor(
ContextCompat.getColor(
this@MainActivity,
R.color.blue_100
)
)
clickable(true)
strokePattern(getSelectedPattern(0))
isFirstLocation = false
}
})
} else {
Log.i(TAG, "getDeviceLocation: locationResult else")
}
} else {
Log.e(TAG, "Exception: %s", task.exception)
}
}
}
ps:请大家多看看代码中的注释,注释已经说的比较明白了。
蓝点方向
蓝点的方向的变化,我们这里就要依靠手机设备的传感器了。通过读取传感器中的数据,来设置蓝点的方向。
关键代码:
首先 获取传感器管理器
mSensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager;
然后 实现SensorEventListener 接口
/加速度传感器数据
private val accelerometerReading = FloatArray(3)
//地磁传感器数据
private val magnetometerReading = FloatArray(3)
//旋转矩阵,用来保存磁场和加速度的数据
private val rotationMatrix = FloatArray(9)
//方向数据
private val orientationAngles = FloatArray(3)
class MainActivity : AppCompatActivity(), OnMapReadyCallback, SensorEventListener {}
接着 注册监听
override fun onResume() {
super.onResume()
//加速度传感器
val accelerometer: Sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
if (accelerometer != null) {
mSensorManager.registerListener(
this,
accelerometer,
SensorManager.SENSOR_DELAY_NORMAL,
SensorManager.SENSOR_DELAY_UI
)
}
//地磁传感器
val magneticField: Sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
if (magneticField != null) {
mSensorManager.registerListener(
this,
magneticField,
SensorManager.SENSOR_DELAY_NORMAL,
SensorManager.SENSOR_DELAY_UI
)
}
}
override fun onPause() {
super.onPause()
mSensorManager.unregisterListener(this);
}
最后 接口实现:
var mPositionMarker: Marker? = null
//var azimuth = 0f
override fun onSensorChanged(event: SensorEvent?) {
//判断sensor类型
if (event!!.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
System.arraycopy(
event.values,
0,
accelerometerReading,
0,
accelerometerReading.size
);
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
System.arraycopy(
event.values,
0,
magnetometerReading,
0,
magnetometerReading.size
);
}
updateOrientationAngles();
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
fun updateOrientationAngles() {
// 更新旋转矩阵.
// 参数1:
// 参数2 :将磁场数据转换进实际的重力坐标中,一般默认情况下可以设置为null
// 参数3:加速度
// 参数4:地磁
SensorManager.getRotationMatrix(
rotationMatrix,
null,
accelerometerReading,
magnetometerReading
)
//根据旋转矩阵计算设备的方向
//参数1:旋转数组
//参数2:模拟方向传感器的数据
SensorManager.getOrientation(rotationMatrix, orientationAngles)
val sb = StringBuilder()
if (orientationAngles.size >= 3) {
sb.append("z轴:${orientationAngles[0]}\n")
sb.append("x轴:${orientationAngles[1]}\n")
sb.append("y轴:${orientationAngles[2]}\n")
//Log.i(TAG, "updateOrientationAngles:sb=>> ${sb.toString()}")
val azimuth = Math.toDegrees(orientationAngles[0].toDouble()).toFloat()
if (mPositionMarker != null) {
mPositionMarker!!.rotation = azimuth
}
}
}
到这里,你已经可以替换蓝点,并能通过获取设备传感器的数据来设置蓝点方向啦。在结合实时定位的功能,去开启导航吧。
说明:篇幅有限,文章中也只是贴出了关键代码。完整项目源码请点击这里:
项目源码地址
参考博客:
官方文档:
https://developers.google.com/maps/documentation/directions/overview
官方demo: