打开滴滴打车APP,会发现地图上的车辆显示的十分形象,车辆会在路上平滑的加减速,转向停车~

有种即时战略的感觉,看起来挺有趣:

20170318102839750.jpg

为了研究这个效果如何实现,我开始了探索之旅:

一.探索阶段:
1.旧版平滑移动
首先找到的是百度“高德地图平滑移动”结果里的这个文:

http://lbs.amap.com/smart/transportation/skill/move/

实现方式是开启子线程,不断执行绘制-销毁图标marker方法来实现看起来在不断向前移动的动画:

20170318103550680.jpg

于是下载了这个DEMO,跑起来,发现确实移动效果不错

但是!!

问题是这个实现方式很难支持多点同时移动

经过实践发现使用这个方法同时操纵多marker时,很大概率只能成功移动最后操作的那个marker,而之前的所有marker都一动不动

努力改造两天之后,依然没有成功保证所有该动的marker都动起来,因此决定另寻他路

2.新版移动
运气还不错,在高德官方文档里找到了17年刚刚发布的新文档:

http://lbs.amap.com/api/android-sdk/guide/draw-on-map/smooth-move
20170318104357761.png

简单浏览了一下这个新文档和DEMO,发现这个实现方式明显封装更良好,也更能保证线程和内存安全

二.开始实现
1.下载最新版Jar包:4.1.3
要想实现这个,必须使用最新版的高德地图SDK,即4.1.3以上版本

在Gradle修改版本,刷新Gradle,搞定

2.会移动的Mark类:SmoothMoveMarker
先看看官方实现的逻辑代码:

// 获取轨迹坐标点
List<LatLng> points = readLatLngs();
LatLngBounds bounds = new LatLngBounds(points.get(0), points.get(points.size() - 2));
mAMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));
 
SmoothMoveMarker smoothMarker = new SmoothMoveMarker(mAMap);
// 设置滑动的图标
smoothMarker.setDescriptor(BitmapDescriptorFactory.fromResource(R.drawable.icon_car));
 
LatLng drivePoint = points.get(0);
Pair<Integer, LatLng> pair = SpatialRelationUtil.calShortestDistancePoint(points, drivePoint);
points.set(pair.first, drivePoint);
List<LatLng> subList = points.subList(pair.first, points.size());
 
// 设置滑动的轨迹左边点
smoothMarker.setPoints(subList);
// 设置滑动的总时间
smoothMarker.setTotalDuration(40);
// 开始滑动
smoothMarker.startSmoothMove();
 

发现高德新增了一个支持移动的Marker类:
SmoothMoveMarker

只要放入轨迹,设定好滑动时间,他就会愉快的滑动起来啦~

3.初步实现多Marker移动
阅读高德demo源码,发现他封装了两个方法来实现移动,一个是移动,另一个是读取路线坐标值:

//平滑移动
public void movePoint() {
    // 获取轨迹坐标点
    List<LatLng> points = readLatLngs();
    LatLngBounds bounds = new LatLngBounds(points.get(0), points.get(points.size() - 2));
    aMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));

    SmoothMoveMarker smoothMarker = new SmoothMoveMarker(aMap);

    // 设置滑动的图标
    smoothMarker.setDescriptor(BitmapDescriptorFactory.fromResource(R.drawable.car_up));
    LatLng drivePoint = points.get(0);
    Pair<Integer, LatLng> pair = SpatialRelationUtil.calShortestDistancePoint(points, drivePoint);
    points.set(pair.first, drivePoint);
    List<LatLng> subList = points.subList(pair.first, points.size());

    // 设置滑动的轨迹左边点
    smoothMarker.setPoints(subList);
    // 设置滑动的总时间
    smoothMarker.setTotalDuration(10);
    // 开始滑动
    smoothMarker.startSmoothMove();
}
//获取路线
private List<LatLng> readLatLngs() {
    List<LatLng> points = new ArrayList<LatLng>();
    for (int i = 0; i < coords.length; i += 2) {
        points.add(new LatLng(coords[i + 1], coords[i]));
    }
    return points;

}

为了观察多点同时移动,决定把这个方法重复实现两次,两条路线不一样

那怎么设定不同的路线呢?

readLatLngs():这个方法读取了名为coords的集合
那么我们就先设定一个deuble[]集合

把路线经过点的各个坐标值放进去

第二次运行的时候再改变这个集合就OK了~

如下例,地图中心点是天安门,两辆车会从天安门西边,一个向南一个向北移动:

double[] newoords = {116.380729, 39.906443,
116.330776, 39.868508};
coords = newoords;
movePoint();
double[] newoords2 = {116.319618, 39.929614,
        116.437377, 39.906443};
coords = newoords2;
movePoint();

经过试验,可以良好的支持多点同时移动

4.加入路线,刷新
搞定了基础实现,接下来就要考虑具体场景了

1.数据源:
由于司机端的数据源信息更新慢(每10秒报一轮GPS),方向信息缺乏(静止时没有方向数据)

考虑到这些原因,我决定把平滑移动的时间改为10秒:

// 设置滑动的总时间
smoothMarker.setTotalDuration(10);
查询附近车辆的接口,也改为每十秒钟循环一次,以达到与司机端发送信息的频率一致

carsHandler.postDelayed(this, 10000);
2.对照车辆ID
如何判断新得到的那些车,有哪几辆是与上次搜索时一样的呢?

首先,必须有车辆ID

看看接口文档,接口数据里除了经纬度之外还带了个car_id,果断拿来对照一下

具体实现时要考虑两种情况:

1.如果car_id相同:平滑移动marker

2.如果car_id不同:展示静态marker

5.问题:如何清除旧marker?
很简单,每次成功请求到新的附近车辆数据时,把AMap对象clear()一下就行咯

缺点是会有瞬间的地图闪动,那一瞬间地图上所有附加的图标点全部清空,不过时间很短不太容易察觉到

效果展示:

111.jpg

三.优化
1.第一次加载时的角度
在写完上述功能后,发现一个问题:

车辆第一次加载在地图上时,marker的角度全都是默认角度:0度,头朝上

这可不太友好,于是问了问司机端,能不能获取一下即时的车辆角度?

回答是:动的时候可以,静止之后不能

于是又问了服务端,能不能做一个“记录车辆最后一次报告的角度”功能

这个当然可以~

于是完美解决了第一次加载车辆marker时的角度问题

2.地图挪动时,太灵敏
解决了角度问题,又发现一个新的问题,就是“地图中心点改变时,如何即时刷新附近车辆?”

高德地图的API给出的监听方法是:

onCameraChangeFinish()

用来在地图被挪动后执行一些方法

之前一直把获取附近车辆的接口方法写在这里,因而造成了:

即使地图只挪了1米远,同样会发出一遍获取附近车辆的请求

这样造成的坏处是:

1.增加服务器负担

2.影响正常marker动画展示,因为前后两次请求如果小于10秒,那么请求到的坐标值是一样的,这样的话车辆就不会移动了

一个简单的解决方案是:

把“请求附近车辆”的方法放进界面onCreate()或者onResume()方法里,用Hnadler继承runnable接口的方式写一个循环执行的线程

这样解决了地图挪动太灵敏的问题,不过也造成了地图挪动后,新位置附近的车加载过慢的新问题

要解决这个也很简单,刚才说到的onCameraChangeFinish()方法,可以在这个方法里销毁一下刚才的Handler并且重新post()一遍就行啦~

哎不对,这岂不是跟刚开始的时候一样了嘛?

淡定,这一步还没做完,我们可以对比一下地图前后中心点的距离,当它大于某个长度后才进行搜索嘛~

这样每次地图挪动之后都会重新执行一下“加载附近车辆”的方法,同时又不用担心加载过于灵敏或者线程重复执行的问题啦

3.渐变展现
在快要完成这项功能的最后,我发现滴滴的车辆展示其实不是立马展示上去的,而是有一个渐隐动画

我决定也做一个类似的效果

用到了经典的安卓动画文件夹:anim

里面可以设定透明度:alpha:

还可以设定展示的时间during:

设定好参数之后呢,在java文件里加载也很方便,

让指定的marker对象:.startAnimation(animationutils.loadanimation(R.anim.xxx_xx));

大功告成~

效果展示:

666.gif