需求:
类似滴滴打车司机接了单,客户端从等待界面跳转到地图界面,代表司机的小车在往自己位置赶,用高德平滑移动实现
要达到的效果:
效果.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就可以了)