笔记20171130-Android仿滴滴地图上小车移动

需求:

类似滴滴打车司机接了单,客户端从等待界面跳转到地图界面,代表司机的小车在往自己位置赶,用高德平滑移动实现

要达到的效果:

效果.gif

相关代码:

activity_test.xml(包含地图的布局)
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="cn.weekimwee.map3d.TestActivity">
    <com.amap.api.maps.MapView
        android:id="@+id/mapView"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_width="0dp"
        android:layout_height="0dp"/>
    <Button
        android:id="@+id/start"
        android:padding="5dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始跑"/>

    <Button
        app:layout_constraintLeft_toRightOf="@id/start"
        android:id="@+id/run"
        android:padding="5dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="模拟司机接到人"/>

</android.support.constraint.ConstraintLayout>
TestActivity.java(本此笔记demo中的示例activity)
package cn.weekimwee.map3d

import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import cn.weekimwee.map3d.wkw.WKWDrivingRouteOverlay
import com.amap.api.maps.AMap
import com.amap.api.maps.model.BitmapDescriptorFactory
import com.amap.api.maps.model.LatLng
import com.amap.api.maps.model.Marker
import com.amap.api.maps.utils.SpatialRelationUtil
import com.amap.api.maps.utils.overlay.SmoothMoveMarker
import com.amap.api.services.core.AMapException
import com.amap.api.services.core.LatLonPoint
import com.amap.api.services.route.*
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_test.*
import org.jetbrains.anko.toast
import java.util.concurrent.TimeUnit


class TestActivity : AppCompatActivity() {

    private var rOverlay: WKWDrivingRouteOverlay?=null //往地图上添加覆盖物的类
    private lateinit var aMap: AMap//地图
    private lateinit var smoothMarker: SmoothMoveMarker//平滑移动的标记(就是那个小车)
    private lateinit var routeSearch: RouteSearch//规划路径的类
    private lateinit var disposable:Disposable
    private val startPoint = LatLonPoint(29.859852, 121.586034)//起点 健康城
    private val throughPoint = LatLonPoint(29.852392, 121.580325) //经停点 儿童公园南门
    private val endPoint = LatLonPoint(29.832915, 121.566666)//终点 印象城

    private var latLonArray = arrayListOf<LatLng>()//这个数组的作用是把规划路线的所有坐标点放进去,用来模拟司机的坐标
    private var runArray = arrayListOf<LatLng>()//每隔五秒更新一次位置(模拟司机已经前进了),从上边的数组remove2个坐标出来放到这个数组中
    private var buttonOnClick = true
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
        mapView.onCreate(savedInstanceState)
        aMap = mapView.map
        smoothMarker = SmoothMoveMarker(aMap)
        //下边是规划路径的逻辑
        routeSearch = RouteSearch(this).apply {
            //开始规划驾车路线
            calculateDriveRouteAsyn(RouteSearch.DriveRouteQuery(RouteSearch.FromAndTo(startPoint, throughPoint), RouteSearch.DrivingDefault, null, null, ""))
            //设置规划完成的监听
            setRouteSearchListener(object : RouteSearch.OnRouteSearchListener {
                override fun onDriveRouteSearched(p0: DriveRouteResult?, p1: Int) {
                    if (p1 == AMapException.CODE_AMAP_SUCCESS) {
                        if(latLonArray.isNotEmpty()) latLonArray.clear()
                        p0?.paths?.let {
                            it[0].steps.forEach {
                                it.polyline.forEach {
                                    latLonArray.add(LatLng(it.latitude,it.longitude))
                                }
                            }
                            //规划路线
                            rOverlay = if(buttonOnClick){
                                WKWDrivingRouteOverlay(aMap,latLonArray).apply {
                                    //模拟司机接单后,未赶到(未接到人)的情况,规划从司机位置到用户位置的路线(要画线)
      
                                    //规划完将起点和终点内的路径放到手机view中间位置
                                    //这个方法要注意下,因为滴滴打车的小车跑的那个activity中, 布局上边有个矩形区域是显示司机信息和位置、价格等信息的,如果
                                    //不用这个方法移动视觉,路线规划就会从mapView最开始的地地方规划,会有部分被遮盖住的
                                    zoomToSpan()
                                }
                            }else{
                                    //模拟接到乘客后,规划从乘客位置到终点的位置(不画线)
                                WKWDrivingRouteOverlay(aMap,LatLng(throughPoint.latitude,throughPoint.longitude),LatLng(endPoint.latitude,endPoint.longitude)).apply {
                                    zoomToSpan()
                                }
                            }
                            runArray.apply {
                                add(latLonArray[0])
                                add(latLonArray[1])
                            }
                            showCarMarker()
                        }
                    } else {
                        toast("未规划到数据")
                    }
                }

                override fun onBusRouteSearched(p0: BusRouteResult?, p1: Int) {
                }

                override fun onRideRouteSearched(p0: RideRouteResult?, p1: Int) {
                }

                override fun onWalkRouteSearched(p0: WalkRouteResult?, p1: Int) {
                }

            })
        }
        //这个按钮是模拟司机接到单的操作,点击了他就会把首次规划的覆盖物清除掉,直接规划乘客位置到终点位置的路线
        run.setOnClickListener {
            buttonOnClick = false
            rOverlay?.onDestroy()
            rOverlay = null
            routeSearch.calculateDriveRouteAsyn(RouteSearch.DriveRouteQuery(RouteSearch.FromAndTo(throughPoint, endPoint), RouteSearch.DrivingDefault, null, null, ""))
        }
        //点击后,模拟不停的调接口查询司机当前位置,5秒执行一次,更新位置移动小车
        start.setOnClickListener {
            if(runArray.isNotEmpty()) runArray.clear()
            //实际上是不停的请求接口  直到数组加载完毕
            disposable = Observable.timer(5, TimeUnit.SECONDS)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .repeat()
                    .subscribe({
                        //开始平滑移动
                        if (latLonArray.size >2) {
                            if (runArray.size != 0) {
                                runArray.removeAt(0)
                                runArray.add(latLonArray.removeAt(0))
                            } else{
                                runArray.add(latLonArray.removeAt(0))
                                runArray.add(latLonArray.removeAt(0))
                            }
                            if (smoothMarker != null) smoothMarker.destroy()
                            showCarMarker()
                        }else{
                            disposable?.dispose()
                        }
                    }, {
                        it.printStackTrace()
                    })
        }
    }

    override fun onResume() {
        super.onResume()
        mapView.onResume()
    }

    override fun onPause() {
        super.onPause()
        mapView.onPause()
    }

    override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }

    override fun onDestroy() {
        super.onDestroy()
        smoothMarker.destroy()
        rOverlay?.onDestroy()
        mapView.onDestroy()
    }
    //添加小车及移动的逻辑
    private fun showCarMarker(){
        smoothMarker.setDescriptor(BitmapDescriptorFactory.fromResource(R.drawable.icon_car))
        // 取轨迹点的第一个点 作为 平滑移动的启动
        val drivePoint = runArray[0]
        val pair = SpatialRelationUtil.calShortestDistancePoint(runArray, drivePoint)
        runArray[pair.first] = drivePoint
        val subList = runArray.subList(pair.first, runArray.size)
        // 设置轨迹点
        smoothMarker.setPoints(subList)
        // 设置平滑移动的总时间  单位  秒
        smoothMarker.setTotalDuration(5)

        // 设置  自定义的InfoWindow 适配器
        aMap.setInfoWindowAdapter(object : AMap.InfoWindowAdapter {
            override fun getInfoWindow(marker: Marker): View? {
                return null
            }

            override fun getInfoContents(marker: Marker): View? {
                return null
            }
        })
        // 显示 infowindow
        smoothMarker.marker.showInfoWindow()
        smoothMarker.startSmoothMove()
    }
}
WKWDrivingRouteOverlay.java(往地图上添加覆盖物的类)
package cn.weekimwee.map3d.wkw;

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 java.util.ArrayList;
import java.util.List;

import cn.weekimwee.map3d.R;

/**
 * Created by Wee Kim Wee on 2017/11/19.
 */

public class WKWDrivingRouteOverlay {

    private AMap aMap;
    private List<LatLng> drivePath;
    private float width = 25;
    private Marker startMarker;
    private Marker endMarker;
    private BitmapDescriptor startBit;
    private BitmapDescriptor endBit;
    private List<Polyline> allPolyLines = new ArrayList();
    private boolean isFirst;
    private PolylineOptions polyLineOptions;
    private LatLng startLatLng;
    private LatLng endLatLng;

    /**
     * 构造方法:司机刚接到单,未接到人时候用的构造方法,要画线
     *
     * @param aMap      地图
     * @param drivePath 从车辆点到接人点的规划路径的经纬度集合
     */
    public WKWDrivingRouteOverlay(AMap aMap, List<LatLng> drivePath) {
        this.aMap = aMap;
        this.drivePath = drivePath;
        this.startLatLng = drivePath.get(0);
        this.endLatLng = drivePath.get(drivePath.size() - 1);
        this.isFirst = true;

        polyLineOptions = new PolylineOptions();
        polyLineOptions.width(width);
        polyLineOptions.setCustomTexture(BitmapDescriptorFactory.fromResource(R.drawable.custtexture));
        endBit = BitmapDescriptorFactory.fromResource(R.drawable.amap_start);
        addMarkerToMap();
        drawLine();
    }

    /**
     * 构造方法:司机已接到人,不画线
     *
     * @param aMap        地图
     * @param startLatLng 起点经纬度
     * @param endLatLng   终点经纬度
     */
    public WKWDrivingRouteOverlay(AMap aMap, LatLng startLatLng, LatLng endLatLng) {
        this.aMap = aMap;
        this.startLatLng = startLatLng;
        this.endLatLng = endLatLng;
        this.isFirst = false;

        startBit = BitmapDescriptorFactory.fromResource(R.drawable.amap_start);
        endBit = BitmapDescriptorFactory.fromResource(R.drawable.amap_end);
        addMarkerToMap();
    }

    /**
     * 将起点终点的marker添加到地图上
     */
    public void addMarkerToMap() {
        try {
            if (isFirst) {
                endMarker = aMap.addMarker(new MarkerOptions().position(drivePath.get(drivePath.size() - 1)).icon(endBit));
            } else {
                startMarker = aMap.addMarker(new MarkerOptions().position(startLatLng).icon(startBit));
                endMarker = aMap.addMarker(new MarkerOptions().position(endLatLng).icon(endBit));
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    /**
     * 画线
     */
    private void drawLine() {
        try {
            for (LatLng latLng : drivePath) {
                polyLineOptions.add(latLng);
            }
            allPolyLines.add(aMap.addPolyline(polyLineOptions));
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * 将点始终保持在能看到的位置
     */
    public void zoomToSpan() {
        try {
            LatLngBounds.Builder b = LatLngBounds.builder();
            b.include(new LatLng(startLatLng.latitude,startLatLng.longitude));
            b.include(new LatLng(endLatLng.latitude,endLatLng.longitude));
            LatLngBounds bounds = b.build();
            //就是这个方法把视觉一直控制在大概中间的位置,具体的可以看高德地图API
            aMap.animateCamera(CameraUpdateFactory.newLatLngBoundsRect(bounds, 100, 100, 400, 350));
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * onDestory
     */
    public void onDestroy() {
        if (startBit != null) startBit.recycle();
        if (endBit != null) endBit.recycle();
        if (startMarker != null) startMarker.destroy();
        if (endMarker != null) endMarker.destroy();
        if (allPolyLines.size() != 0) {
            for (Polyline line : allPolyLines) {
                line.remove();
            }
        }
    }
}

实现步骤:

  • 先熟悉高德地图,用到了地图SDK(3D)和定位SDK(有示例代码)
  • 按照API文档中所述,将各种so库和jar包引入工程,高德地图官方教程
  • 按上述所说的完成后,先成功将地图显示出来,一般视觉会移到当前城市
  • 确定好起点终点和经过点(这个非必要,有需要就留着,没需要不用,滴滴上呼叫司机的客户的位置就是经过点,不过标成了起点),我这个demo上起点终点经过点是在这里地图上查了几个
  • 规划驾车路线,就是代码里这个类执行的“routeSearch.calculateDriveRouteAsyn”一系列操作
  • 查出来之后,如果是刚接单(模拟司机接单后从等待页面到地图页面时的首次规划),这时候是司机往乘客自己的位置赶,那么乘客自己的位置就是本次规划的终点,但是终点的marker要换成“起”这个图标,因为在乘客打车的角度,我在的位置才是起点,但是从开发的角度规划的司机的位置才是起点,司机初始位置就不要起点图标了,把小车显示出来
  • 每隔5秒(按实际需求)来更新一下司机位置,高德平滑移动是从一个点到另一个点的移动,也就是runArray每隔五秒remove一个0位置的经纬度,再添加一下最新的经纬度,然后如果smoothMarker不是空的话就ondestory清除一下,再showCarMarker(),空的话就直接showCarMarker(),这样如果司机一直在走的话,就会看到每小车再动了。注意:showCarMarker()方法中有一句话 “smoothMarker.setTotalDuration(5)”,这是设置两点间用5秒滑动完毕,这个duration的值建议设置与更新司机位置的时间一致
  • 然后司机接到人之后,客户端收到通知(demo中是点一下按钮模拟司机接单了),就再规划一次,这次是从起点规划到终点。注意:这次规划完是不画线的(滴滴司机接到人后就没画),一般司机接乘客时与乘客的距离比乘客到终点的距离近,因为是通知的附近的车,所以司机不按规划走的风险很小(因为一般太近了很可能就一两条路),但是乘客到终点的距离一般是比较远的,只规划了一次是为了画上起点和终点的marker(就是“起”图标和“终”图标),因为只规划了一次,不能实时的反映交通状况,司机可能会在走的过程中根据路况不同而选择与规划不一样的路线,如果画上线会给人一种司机不按路线走,app也会感觉比较死板(这是我个人理解),所以就只平滑移动即可,反正司机即使不按规划路线走,小车也是在“起”和“终”之间走,而不会因为画了路线,造成如果不按路线走,小车就偏离了蓝色的线很难看的效果

以上是关于地图上小车平滑移动的笔记。demo在这里(如果下载来看,需要自己去高德后台配置key,然后在demo里更换上自己配置的key就可以了)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容