路线规划功能,主要是调用高德开放平台的 RouteSearch 功能; 详细请看高德驾车路线规划文档说明:
此篇文章主要介绍路线绘制和气泡绘制效果。 UI效果如下图:
一、多条路线绘制在地图上: 高德的导航Demo提供了单条路线的绘制。 我们可以在导航Demo的基础上实现自己的多条路线绘制。 源码如下:
高德Demo的RouteOverlay 图层定义:
package com.xxx;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import com.amap.api.maps.AMap;
import com.amap.api.maps.CameraUpdateFactory;
import com.amap.api.maps.model.BitmapDescriptor;
import com.amap.api.maps.model.BitmapDescriptorFactory;
import com.amap.api.maps.model.LatLng;
import com.amap.api.maps.model.LatLngBounds;
import com.amap.api.maps.model.Marker;
import com.amap.api.maps.model.MarkerOptions;
import com.amap.api.maps.model.Polyline;
import com.amap.api.maps.model.PolylineOptions;
import com.xxx.R;
import java.util.ArrayList;
import java.util.List;
public class RouteOverlay {
protected List<Marker> stationMarkers = new ArrayList<Marker>();
protected List<Polyline> allPolyLines = new ArrayList<Polyline>();
protected Marker lineMarker; //TODO
protected Marker startMarker;
protected Marker endMarker;
protected LatLng linePoint;
protected LatLng startPoint;
protected LatLng endPoint;
protected AMap mAMap;
private Context mContext;
private Bitmap startBit, endBit, busBit, walkBit, driveBit;
protected boolean nodeIconVisible = true;
protected int linePosition;
public RouteOverlay(Context context) {
mContext = context;
}
/**
* 去掉BusRouteOverlay上所有的Marker。
*
* @since V2.1.0
*/
public void removeFromMap() {
if (startMarker != null) {
startMarker.remove();
}
if (endMarker != null) {
endMarker.remove();
}
for (Marker marker : stationMarkers) {
marker.remove();
}
for (Polyline line : allPolyLines) {
line.remove();
}
destroyBit();
}
private void destroyBit() {
if (startBit != null) {
startBit.recycle();
startBit = null;
}
if (endBit != null) {
endBit.recycle();
endBit = null;
}
if (busBit != null) {
busBit.recycle();
busBit = null;
}
if (walkBit != null) {
walkBit.recycle();
walkBit = null;
}
if (driveBit != null) {
driveBit.recycle();
driveBit = null;
}
}
/**
* 给路线Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
*
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getLineBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.shape_white_15);
}
/**
* 给起点Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
*
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getStartBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.path_start);
}
/**
* 给终点Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
*
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getEndBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.path_end);
}
/**
* 给公交Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
*
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getBusBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_bus);
}
/**
* 给步行Marker设置图标,并返回更换图标的图片。如不用默认图片,需要重写此方法。
*
* @return 更换的Marker图片。
* @since V2.1.0
*/
protected BitmapDescriptor getWalkBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_man);
}
protected BitmapDescriptor getDriveBitmapDescriptor() {
return BitmapDescriptorFactory.fromResource(R.drawable.amap_car);
}
protected void addStartAndEndMarker() {
startMarker = mAMap.addMarker((new MarkerOptions())
.position(startPoint).icon(getStartBitmapDescriptor())
.anchor(0.5f, 0.5f));
startMarker.setClickable(true);
endMarker = mAMap.addMarker((new MarkerOptions()).position(endPoint)
.anchor(0.5f, 0.5f)
.icon(getEndBitmapDescriptor()));
endMarker.setClickable(true);
}
/**
* 移动镜头到当前的视角。
*
* @since V2.1.0
*/
public void zoomToSpan() {
if (startPoint != null) {
if (mAMap == null) {
return;
}
try {
LatLngBounds bounds = getLatLngBounds();
mAMap.animateCamera(CameraUpdateFactory
.newLatLngBounds(bounds, 100));
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public void zoomToSpan(int paddingLeft,
int paddingRight,
int paddingTop,
int paddingBottom) {
if (startPoint != null) {
if (mAMap == null) {
return;
}
try {
LatLngBounds bounds = getLatLngBounds();
mAMap.animateCamera(CameraUpdateFactory
.newLatLngBoundsRect(bounds, paddingLeft, paddingRight, paddingTop, paddingBottom));
} catch (Throwable e) {
e.printStackTrace();
}
}
}
protected LatLngBounds getLatLngBounds() {
LatLngBounds.Builder b = LatLngBounds.builder();
b.include(new LatLng(startPoint.latitude, startPoint.longitude));
b.include(new LatLng(endPoint.latitude, endPoint.longitude));
return b.build();
}
/**
* 路段节点图标控制显示接口。
*
* @param visible true为显示节点图标,false为不显示。
* @since V2.3.1
*/
public void setNodeIconVisibility(boolean visible) {
try {
nodeIconVisible = visible;
if (this.stationMarkers != null && this.stationMarkers.size() > 0) {
for (int i = 0; i < this.stationMarkers.size(); i++) {
this.stationMarkers.get(i).setVisible(visible);
}
}
} catch (Throwable e) {
e.printStackTrace();
}
}
protected void addStationMarker(MarkerOptions options) {
if (options == null) {
return;
}
Marker marker = mAMap.addMarker(options);
if (marker != null) {
stationMarkers.add(marker);
}
}
protected Polyline addPolyLine(PolylineOptions options) {
if (options == null) {
return null;
}
Polyline polyline = mAMap.addPolyline(options);
if (polyline != null) {
allPolyLines.add(polyline);
}
return polyline;
}
protected float getRouteWidth() {
return 25f;
}
protected int getWalkColor() {
return Color.parseColor("#6db74d");
}
/**
* 自定义路线颜色。
* return 自定义路线颜色。
*
* @since V2.2.1
*/
protected int getBusColor() {
return Color.parseColor("#537edc");
}
protected int getDriveColor() {
return Color.parseColor("#537edc");
}
}
单条线路绘制:
package com.xxx
import android.content.Context
import android.graphics.Color
import com.amap.api.maps.AMap
import com.amap.api.maps.model.*
import com.amap.api.services.core.LatLonPoint
import com.amap.api.services.route.DrivePath
import com.amap.api.services.route.TMC
import com.xxx.R
import com.xxx.AMapUtil
class DrivingRouteLine(context: Context, val drivePath: DrivePath) :
RouteOverlay(context) {
private var mPolylineOptions: PolylineOptions? = null
private var mPolylineOptionsColor: PolylineOptions? = null
var mPolyline: Polyline? = null
var mColorPolyline: Polyline? = null
var isColorfulline: Boolean = false
var customTextureRes: Int = R.drawable.path_select_direction_texture
var zSetIndex: Int = 0
fun drawLine(aMap: AMap) {
this.mAMap = aMap;
initPolylineOptions()
val mLatLngsOfPath = ArrayList<LatLng>()
val tmcs = ArrayList<TMC>()
val drivePaths = drivePath.steps
for (step in drivePaths) {
val latlonPoints = step.polyline
val tmclist = step.tmCs
tmcs.addAll(tmclist)
for (latlonpoint in latlonPoints) {
mPolylineOptions!!.add(convertToLatLng(latlonpoint))
mLatLngsOfPath.add(convertToLatLng(latlonpoint))
}
}
if (isColorfulline && tmcs.size > 0) {
colorWayUpdate(tmcs)
showColorPolyline()
} else {
showPolyline()
}
}
private fun showPolyline() {
val polyline = addPolyLine(mPolylineOptions)
this.mPolyline = polyline
}
private fun showColorPolyline() {
val polyline = addPolyLine(mPolylineOptionsColor)
this.mColorPolyline = polyline
}
fun getShowPolyline(): Polyline? {
return mColorPolyline ?: mPolyline
}
/**
* 根据不同的路段拥堵情况展示不同的颜色
*
* @param tmcSection
*/
private fun colorWayUpdate(tmcSection: List<TMC>?) {
if (tmcSection.isNullOrEmpty()) {
return
}
var segmentTrafficStatus: TMC
mPolylineOptionsColor = null
mPolylineOptionsColor = PolylineOptions()
mPolylineOptionsColor!!.width(routeWidth)
val colorList: MutableList<Int> = java.util.ArrayList()
mPolylineOptionsColor!!.add(AMapUtil.convertToLatLng(tmcSection[0].polyline[0]))
colorList.add(driveColor)
for (i in tmcSection.indices) {
segmentTrafficStatus = tmcSection[i]
val color: Int = getColor(segmentTrafficStatus.status)
val mployline = segmentTrafficStatus.polyline
for (j in 1 until mployline.size) {
mPolylineOptionsColor!!.add(AMapUtil.convertToLatLng(mployline[j]))
colorList.add(color)
}
}
colorList.add(driveColor)
mPolylineOptionsColor!!.colorValues(colorList)
}
/**
* 初始化线段属性
*/
private fun initPolylineOptions() {
mPolylineOptions = null
mPolylineOptions = PolylineOptions()
mPolylineOptions!!
.color(driveColor)
.setCustomTexture(BitmapDescriptorFactory.fromResource(customTextureRes))
.zIndex(zSetIndex.toFloat())
.width(routeWidth)
}
fun convertToLatLng(point: LatLonPoint): LatLng {
return LatLng(point.latitude, point.longitude)
}
private fun getColor(status: String): Int {
return if (status == "畅通") {
Color.GREEN
} else if (status == "缓行") {
Color.YELLOW
} else if (status == "拥堵") {
Color.RED
} else if (status == "严重拥堵") {
Color.parseColor("#990033")
} else {
Color.parseColor("#537edc")
}
}
fun setZIndex(var1: Int) {
try {
this.zSetIndex = var1
if (mPolyline != null) {
mPolyline!!.zIndex = var1.toFloat()
}
if (mColorPolyline != null) {
mColorPolyline!!.zIndex = var1.toFloat()
}
} catch (var2: Throwable) {
var2.printStackTrace()
}
}
fun setCustomTexture(var1: BitmapDescriptor) {
try {
if (mPolylineOptions != null) {
mPolylineOptions!!.customTexture = var1
}
if (mPolyline != null) {
mPolyline!!.setCustomTexture(var1)
}
if (mColorPolyline != null) {
mColorPolyline!!.setCustomTexture(var1)
}
if (mPolylineOptionsColor != null) {
mPolylineOptionsColor!!.setCustomTexture(var1)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
多条路线的绘制层:
package com.xxx
import android.content.Context
import com.amap.api.maps.AMap
import com.amap.api.maps.model.*
import com.amap.api.services.core.LatLonPoint
import com.amap.api.services.route.DriveRouteResult
import com.xxx.R
import com.xxx.DrivingRouteLine
import com.xxx.AMapUtil
import com.xxx.bean.POIType
import com.xxx.bean.SearchPOI
/**
* 驾车出行路线绘制到地图页
*/
class DrivingRouteListOverlay(
val mContext: Context,
amap: AMap,
result: DriveRouteResult?,
val throughPointList: List<SearchPOI>?,
index: Int
) : RouteOverlay(
mContext
) {
var driveRouteResult: DriveRouteResult?
var isColorfulline = false
private var selectIndex = 0
private val throughPointMarkerList: ArrayList<Marker> = java.util.ArrayList()
private val routeLineList = ArrayList<DrivingRouteLine>()
private var zIndex = 1
private var pathStartMarker: Marker? = null;
private var pathEndMarker: Marker? = null;
private var pathStart: LatLonPoint? = null;
private var pathEnd: LatLonPoint? = null;
/**
* 添加驾车路线添加到地图上显示。
*/
fun addToMap() {
try {
if (mAMap == null) {
return
}
if (routeWidth == 0f) {
return
}
if (driveRouteResult?.paths == null) {
return
}
if (startMarker != null) {
startMarker.remove()
startMarker = null
}
if (endMarker != null) {
endMarker.remove()
endMarker = null
}
for (throughMarker in throughPointMarkerList) {
throughMarker.remove()
}
routeLineList.clear()
for (i in 0 until driveRouteResult!!.paths.size) {
val drivePath = driveRouteResult!!.paths[i]
pathStart = pathStart ?: drivePath.steps.getOrNull(0)?.polyline?.getOrNull(0);
pathEnd = pathEnd ?: drivePath.steps.lastOrNull()?.polyline?.lastOrNull()
val line = DrivingRouteLine(mContext, drivePath)
line.isColorfulline = isColorfulline
line.customTextureRes =
if (i == selectIndex) R.drawable.path_select_direction_texture else R.drawable.path_other_texture
if (i == selectIndex) {
line.setZIndex(zIndex++)
}
line.drawLine(mAMap)
routeLineList.add(line)
}
pathStartMarker?.remove()
pathEndMarker?.remove()
pathStart?.let {
pathStartMarker = mAMap.addMarker(
MarkerOptions()
.position(LatLng(it.latitude, it.longitude)).icon(getPathStartBitDes())
.anchor(0.5f, 0.5f)
)
pathStartMarker?.isClickable = false
}
pathEnd?.let {
pathEndMarker = mAMap.addMarker(
MarkerOptions()
.position(LatLng(it.latitude, it.longitude)).icon(getPathEndBitDes())
.anchor(0.5f, 0.5f)
)
pathEndMarker?.isClickable = false
}
addStartAndEndMarker()
addThroughPointMarker()
} catch (e: Throwable) {
e.printStackTrace()
}
}
fun selectedPolyline(line: Polyline): Int {
val index = findRouteLineIndex(line)
if (index >= 0) {
selectedRoute(index)
}
return index
}
private fun findRouteLineIndex(line: Polyline): Int {
for (i in 0 until routeLineList.size) {
if (routeLineList[i].mPolyline?.id == line.id) {
return i
}
}
return -1
}
fun selectedRoute(index: Int) {
for (item in routeLineList) {
item.setCustomTexture(BitmapDescriptorFactory.fromResource(R.drawable.path_other_texture))
}
routeLineList.getOrNull(index)?.let {
this.selectIndex = index
it.setCustomTexture(BitmapDescriptorFactory.fromResource(R.drawable.path_select_direction_texture))
it.setZIndex(zIndex++)
}
}
private fun addThroughPointMarker() {
if (!throughPointList.isNullOrEmpty()) {
for (i in throughPointList.indices) {
val poi = throughPointList[i]
val icon = if (poi.poiType == POIType.charge) {
getChargePointBitDes()
} else getThroughPointBitDes()
val mOption = MarkerOptions()
.position(LatLng(poi.lat, poi.lng))
.visible(true)
.anchor(0.5f, 0.5f)
.icon(icon)
.draggable(false)
val m = mAMap.addMarker(mOption)
m.isClickable = true;
throughPointMarkerList.add(m)
}
}
}
private fun getPathStartBitDes(): BitmapDescriptor? {
return BitmapDescriptorFactory.fromResource(R.drawable.path_start_marker)
}
private fun getPathEndBitDes(): BitmapDescriptor? {
return BitmapDescriptorFactory.fromResource(R.drawable.path_end_marker)
}
private fun getThroughPointBitDes(): BitmapDescriptor? {
return BitmapDescriptorFactory.fromResource(R.drawable.path_through)
}
private fun getChargePointBitDes(): BitmapDescriptor? {
return BitmapDescriptorFactory.fromResource(R.drawable.path_charge)
}
override fun getLatLngBounds(): LatLngBounds? {
val b = LatLngBounds.builder()
b.include(LatLng(startPoint.latitude, startPoint.longitude))
b.include(LatLng(endPoint.latitude, endPoint.longitude))
if (throughPointList != null && throughPointList.isNotEmpty()) {
for (i in throughPointList.indices) {
b.include(LatLng(throughPointList[i].lat, throughPointList[i].lng))
}
}
return b.build()
}
init {
mAMap = amap
driveRouteResult = result
selectIndex = index
if (result != null) {
startPoint = AMapUtil.convertToLatLng(result.startPos)
endPoint = AMapUtil.convertToLatLng(result.targetPos)
}
}
}
二、气泡图层的绘制(气泡和点的间距通过透明的View来呈现):
package com.xxx
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import com.amap.api.maps.AMap
import com.amap.api.maps.model.BitmapDescriptor
import com.amap.api.maps.model.BitmapDescriptorFactory
import com.amap.api.services.core.PoiItem
import com.xxx.databinding.LocationEndPaopaoBinding
import com.xxx.databinding.LocationStartPaopaoBinding
/**
* 起点终点的气泡弹窗
*/
class StartEndPoiOverlay(
val context: Context,
val amap: AMap,
val pois: List<PoiItem> = ArrayList<PoiItem>()
) :
PoiOverlay(amap, pois) {
fun drawEnd(endTimeDesc: String) {
clear()
pois.getOrNull(1)?.typeDes = endTimeDesc;
addPoiToMap(1)
}
fun drawStart(startTimeDesc: String) {
clear()
pois.getOrNull(0)?.typeDes = startTimeDesc;
addPoiToMap(0)
}
fun clear() {
removeFromMap()
}
override fun getBitmapDescriptor(index: Int): BitmapDescriptor {
var view: View? = null
if (index == 1) {
//ccc
val binding = LocationEndPaopaoBinding.inflate(LayoutInflater.from(context))
binding.tvTimeInfo.text = pois.getOrNull(index)?.typeDes ?: "终点时间"
view = binding.root
} else {
//起点
val binding = LocationStartPaopaoBinding.inflate(LayoutInflater.from(context))
binding.tvTimeInfo.text = pois.getOrNull(index)?.typeDes ?: "起点时间"
view = binding.root
}
return BitmapDescriptorFactory.fromView(view);
}
override fun getAnchor(index: Int): FloatArray {
val array = FloatArray(2)
if (index == 1) {
array[0] = 0.5f
array[1] = 1f
} else {
array[0] = 0.5f
array[1] = 0f
}
return array
}
}
最后贴上算路结果调用绘制的代码片段:
mViewModel.routePathOb.observe(this) { result ->
this.routeCaculateResult = result
mMap!!.clear()
startEndPaoPaoMarker?.clear()
result?.gaoDeiRouteResult?.let {
startEndPaoPaoMarker = StartEndPoiOverlay(context, mMap, getStartEndPoiItems(it))
}
val paths = result?.gaoDeiRouteResult?.paths
if (!paths.isNullOrEmpty()) {
val routeLine = DrivingRouteListOverlay(
requireContext(),
mMap,
result.gaoDeiRouteResult,
mPassPoint,
mViewModel.mSelectedRoutePlanIndex
)
//绘制多条线路
routeLine.addToMap()
val paddingTop = mBinding.tripLocationView.bottom
val bottom = SizeUtils.dp2px(228f)
val centerX = width / 2
val centerY = paddingTop + (ScreenUtils.getScreenHeight() - bottom - paddingTop) / 2
val showPaoPaoWidth = SizeUtils.dp2px(98f)
val showPaoPaoHeight = SizeUtils.dp2px(88f)
val dW = showPaoPaoWidth / 2;
val dH = showPaoPaoHeight;
val dRect = getPaoPaoShowFullRect(result.gaoDeiRouteResult)
//根据页面布局设置地图中心点
mMap!!.setPointToCenter(centerX, centerY)
//显示完整的路线缩放比例尺度
routeLine.zoomToSpan(
100 + dW * dRect.left,
100 + dW * dRect.right,
paddingTop + 50 + dH * dRect.top,
bottom + 100 + dH * dRect.bottom
)
routePathOverlay = routeLine
//绘制起点或终点事件泡泡
val pathTime = getIndexPathTime(it,mViewModel.mSelectedRoutePlanIndex)
if (mViewModel.isStartTripTime) {
startEndPaoPaoMarker?.drawEnd(pathTime)
} else {
startEndPaoPaoMarker?.drawStart(pathTime)
}
} else {
Logger.e(TAG, "paths is Null or Empty")
}
}