Android-仿高德的路线规划

436 阅读5分钟

路线规划功能,主要是调用高德开放平台的 RouteSearch 功能; 详细请看高德驾车路线规划文档说明:

此篇文章主要介绍路线绘制和气泡绘制效果。 UI效果如下图:

image.png

一、多条路线绘制在地图上: 高德的导航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")
    }
}