查看原文
其他

Android车辆运动轨迹平滑移动(高仿滴滴打车)最佳实践

码农西哥 技术最TOP 2022-08-26

点击上方“Android技术杂货铺”,选择“标星”

干货文章,第一时间送达!


作者:门心叼龙 链接:https://www.jianshu.com/p/015bd44a56e6

关于车辆运动的相关文章一共写过两篇,一篇为Android车辆运动轨迹大数据采集最佳实践,另外一篇是Android车辆运动轨迹数据采集服务保活的探索与发现

一直打算写一篇车辆运动轨迹平滑移动的文章,年后由于工作项目太忙也就没时间写,工作的事情忙完了,紧接着就是忙自己的另外两个开源项目,一个是Android客户端框架FlyTour、和另外一个SpringCloud微服务框架FlyCloud,上个周终于告一段落。

数据也能采集了,而且采集服务保活也做了,有效的避免了数据采集服务在后台被系统杀死的可能,接着就需要把采集到的数据在移动设备上实时的展示出来,这两天我也特意在网上搜索了一下,关于Android车辆运动轨迹平滑移动的文章,还真没有,大部分都是提问的多,问怎么实现的?都是一些只言片语很零碎的一些回答,在实战项目当中没有太大的实用价值。

关于车辆运动,在我们在日常生活中见到最多的就是滴滴打车,想必这款app大家都使用过,当你在app的叫车页面,输入完毕你的目的地,点击叫车,如果有司机接单了,你就清楚的看到车辆会平滑的移动到你所在的位置去接你。今天我就带领大家一步步的实现一个车辆平滑移动的功能。

这个功能是基于高德地图开发的,因此我特意去高德官网查阅了一下,高德的确提供了官方轨迹移动api,我们暂且不用官方API.先用自己的方法去实现一个最简单的功能 我们先来一睹为快:

单次轨迹回放

已知有一段轨迹数据,点击回放按钮,小车沿着路线自动的往前运动,播放完毕也就结束了

public class MoveSingleThread extends Thread{
private List<LatLng> mLatLngList;
private Marker mCarMarker;
public MoveSingleThread(List<LatLng> latLngs, Marker marker) {
super();
mLatLngList = latLngs;
mCarMarker = marker;
}

@Override
public void run() {
super.run();
}
public void moveTrack(){
// 第一个for循环用来计算走了多少部
int step = 0;
for (int i = 0; i < mLatLngList.size() - 1; i++) {
LatLng startPoint = mLatLngList.get(i);
LatLng endPoint = mLatLngList.get(i + 1);
double slope = getSlope(startPoint, endPoint);
// 是不是正向的标示(向上设为正向)
boolean isReverse = (startPoint.latitude > endPoint.latitude);
double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
// 应该对经纬度同时处理
for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
j - xMoveDistance) {
step++;
}
}

// 通过距离,计算轨迹动画时间间隔
double mTimeInterval = 0;// 轨迹回放时间戳
if (!TextUtils.isEmpty(mDistance)) {
float totalDistance = Float.parseFloat(mDistance) * 1000;
if (totalDistance <= 500) {
mTimeInterval = 1000.0 / step;
} else if (totalDistance > 500 && totalDistance <= 7500) {
mTimeInterval = 2.0 * totalDistance / step;
} else {
mTimeInterval = 15000.0 / step;
}
}

// while (true) {
for (int i = 0; i < mLatLngList.size() - 1; i++) {
if (stopFlag) {
stopFlag = false;
break;
}
mIsCarMoveing = true;
LatLng startPoint = mLatLngList.get(i);
LatLng endPoint = mLatLngList.get(i + 1);
mCarMarker.setPosition(startPoint);
mCarMarker.setRotateAngle((float) getAngle(startPoint, endPoint));
double slope = getSlope(startPoint, endPoint);
// 是不是正向的标示(向上设为正向)
boolean isReverse = (startPoint.latitude > endPoint.latitude);
double intercept = getInterception(slope, startPoint);
double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
// 应该对经纬度同时处理
double mSleep = 0;
for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
j - xMoveDistance) {
LatLng latLng = null;
if (slope != Double.MAX_VALUE) {
latLng = new LatLng(j, (j - intercept) / slope);
// latLng = new LatLng(j, k);
} else {
latLng = new LatLng(j, startPoint.longitude);
}
mCarMarker.setPosition(latLng);
// 如果间隔时间小于1毫秒,则略过当前休眠,累加直到休眠时间到1毫秒:会损失精度
if (mTimeInterval < 1) {
mSleep += mTimeInterval;
if (mSleep >= 1) {
SystemClock.sleep((long) mSleep);
mSleep = 0;
}
} else
SystemClock.sleep((long) mTimeInterval);
}
}
}
}

实时轨迹数据排队问题

如果要显示实时轨迹怎么办 ,上面的代码就有问题了,Thread.start()方法调用后,就会立马执行他的run方法,run方法执行完毕,线程也就结束了,也是说上面的代码只能跑一次轨迹数据,如果每间隔五秒从后台取一次轨迹数据,就需要一数据队列来存储这些数据,每跑完一次数据,就从数据队列里面去取,如果有就取来接着跑,如果没有就处于等待状态。

我们创建异步消息处理线程,这一问题就可以迎刃而解,来数据了我们就可以通过handler把数据post给我们的子线程,Handler自带数据队列,它处于排队状态,如果有数据了就开始跑轨迹,如果没有数据就处于等待状态,直到有数据的到来,如果对异步消息处理线程不熟悉,请查看我的另外一篇文章《Android实战开发Handler机制深度解析》https://menxindiaolong.blog.csdn.net/article/details/86560330

一个标准的异步消息处理线程应该怎么写?

方法1:

class LooperThread extends Thread {
public Handler mHandler;

public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
Looper.loop();
}
}

方法2:

// Step 1: 创建并启动HandlerThread线程,内部包含Looper
HandlerThread handlerThread = new HandlerThread("gityuan.com");
handlerThread.start();

// Step 2: 创建Handler
Handler handler = new Handler(handlerThread.getLooper()) {
public void handleMessage(Message msg) {
// process incoming messages here
}
};
// Step 3: 发送消息
handler.post(new Runnable() {
@Override
public void run() {
System.out.println("thread id="+Thread.currentThread().getId());
}
});

上面就是Android系统中异步消息处理线程的通用写法

运动轨迹的暂停、继续问题

由于运动轨迹是在子线程里面完成的,我们自然而然会想到线程的等待、唤醒,也就是waitnotify的问题了 因此我们在运动过程加上就如下代码就可以了

if (pause) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

怎么让他恢复运动呢?notify一下即可

public void reStartMove() {
synchronized (lock) {
pause = false;
lock.notify();
}
}


完整的代码如下:

public class MoveCarCustomThread extends Thread {
public static final String TAG = MoveCarCustomThread.class.getSimpleName();
private Handler moveCarHandler;//发送数据的异步消息处理器
private Object lock = new Object();//线程锁
private boolean moveing = false;//是否线程正在移动
private boolean pause = false;//暂停状态,为true则暂停
private boolean stop = false;//停止状态,为true则停止移动
private WeakReference<MainActivity> mActivityWeakReference;//防止内存Activity导致的内容泄漏
private MOVE_STATE currMoveState = MOVE_STATE.START_STATUS;

public void setCurrMoveState(MOVE_STATE currMoveState) {
this.currMoveState = currMoveState;
}

public MOVE_STATE getCurrMoveState() {
return currMoveState;
}
public MoveCarCustomThread(MainActivity activity) {
mActivityWeakReference = new WeakReference<>(activity);
}
//暂停移动
public void pauseMove() {
pause = true;
}
//设置暂停之后,再次移动调用它
public void reStartMove() {
synchronized (lock) {
pause = false;
lock.notify();
}
}

public void stopMove() {
stop = true;
if(moveCarHandler != null){
moveCarHandler.removeCallbacksAndMessages(null);
}
if(mActivityWeakReference.get() != null){
mActivityWeakReference.get().mLatLngList.clear();
mActivityWeakReference.get().mMainHandler.removeCallbacksAndMessages(null);
}
}

public Handler getMoveCarHandler() {
return moveCarHandler;
}
public boolean isMoveing() {
return moveing;
}


@Override
public void run() {
super.run();
//设置该线程为loop线程
Looper.prepare();
moveCarHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//通过锁保证发过来的数据同步入列
synchronized (lock) {
if (msg.obj != null && msg.obj instanceof List) {
List<LatLng> latLngList = (List<LatLng>) msg.obj;
moveCoarseTrack(latLngList);
}
}
}
};
//启动loop线程
Looper.loop();
}

private void moveCoarseTrack(List<LatLng> latLngList) {
if (latLngList == null || latLngList.size() == 0 || latLngList.size() == 1) {
return;
}
Log.v(TAG, "moveCoarseTrack start.........................................................");
long startTime = System.currentTimeMillis();
Log.v(TAG, "startTime:" + startTime);
int step = TrackMoveUtil.getStep(latLngList);// 通过距离,计算轨迹动画运动步数
Log.v(TAG, "move step:" + step);
float distance = TrackMoveUtil.getDistance(latLngList);
Log.v(TAG, "move distance:" + distance);
double mTimeInterval = TrackMoveUtil.getMoveTime(distance, step);// 通过距离,计算轨迹动画时间间隔
mTimeInterval = 10;// 每走一步停止10毫秒
Log.v(TAG, "move mTimeInterval:" + mTimeInterval);

moveing = true;
for (int i = 0; i < latLngList.size() - 1; i++) {
// 暂停状态,线程停止了
if (pause) {
movePause();
}
if (stop) {
break;
}
moveing = true;
LatLng startLatLng = latLngList.get(i);
LatLng endLatLng = latLngList.get(i + 1);
MainActivity mainActivity = mActivityWeakReference.get();
moveCar(startLatLng, endLatLng, mainActivity);
moveLine(startLatLng, mainActivity);
moveCamera(startLatLng, mainActivity);

double slope = TrackMoveUtil.getSlope(startLatLng, endLatLng);// 计算两点间的斜率
double intercept = TrackMoveUtil.getInterception(slope, startLatLng);// 根据点和斜率算取截距
boolean isReverse = (startLatLng.latitude > endLatLng.latitude);// 是不是正向的标示(向上设为正向)
double xMoveDistance = isReverse ? TrackMoveUtil.getXMoveDistance(slope) : -1 * TrackMoveUtil.getXMoveDistance(slope);
// 应该对经纬度同时处理
double sleep = 0;
int flag = 0;
for (double j = startLatLng.latitude; !((j >= endLatLng.latitude) ^ isReverse); j = j - xMoveDistance) {
// 非暂停状态地图才进行跟随移动
if (pause) {
movePause();
}
if (stop) {
break;
}
moveing = true;
flag++;
if (slope != Double.MAX_VALUE) {
startLatLng = new LatLng(j, (j - intercept) / slope);
} else {
startLatLng = new LatLng(j, startLatLng.longitude);
}
moveCar(startLatLng, mainActivity);
moveLine(startLatLng, mainActivity);
if (flag % 100 == 0) {
moveCamera(startLatLng, mainActivity);
}
// 如果间隔时间小于1毫秒,则略过当前休眠,累加直到休眠时间到1毫秒:会损失精度
if (mTimeInterval < 1) {
sleep += mTimeInterval;
if (sleep >= 1) {
Log.v(TAG, "sleep:" + sleep);
SystemClock.sleep((long) sleep);
sleep = 0;
}
} else {
SystemClock.sleep((long) mTimeInterval);
}

}
}
long endTime = System.currentTimeMillis();
moveing = false;
Log.v(TAG, "endTime:" + endTime);
Log.v(TAG, "run mTimeInterval:" + (endTime - startTime));
Log.v(TAG, "moveCoarseTrack end.........................................................");
}

private void moveLine(LatLng startLatLng, MainActivity mainActivity) {
mainActivity.mLatLngList.add(startLatLng);// 向轨迹集合增加轨迹点
mainActivity.mMovePolyline.setPoints(mainActivity.mLatLngList);// 轨迹画线开始
}

private void moveCar(LatLng startLatLng, LatLng endLatLng, MainActivity mainActivity) {
moveCar(startLatLng,mainActivity);
if (mainActivity.mCarMarker != null) {
mainActivity.mCarMarker.setRotateAngle((float) TrackMoveUtil.getAngle(startLatLng, endLatLng));// 设置小车车头的方向
}
}
private void moveCar(LatLng startLatLng,MainActivity mainActivity) {
if (mainActivity.mCarMarker != null) {
mainActivity.mCarMarker.setPosition(startLatLng);// 小车移动
}
}
private void movePause() {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private void moveCamera(LatLng startLatLng, MainActivity mainActivity) {
Message message = Message.obtain();
message.what = MainActivity.EventType.MapMove;
message.obj = startLatLng;
mainActivity.mMainHandler.sendMessage(message);
}

}

核心算法工具类

* <h1>轨迹平滑所需要的工具方法</h1> Date: 2016-10-27 Created by mxdl
*/
public class TrackMoveUtil {
private static double DISTANCE = 0.0001;

/**
* 根据两点算斜率
*/

public static double getSlope(LatLng fromPoint, LatLng toPoint) {
if (fromPoint == null || toPoint == null) {
return 0;
}
if (toPoint.longitude == fromPoint.longitude) {
return Double.MAX_VALUE;
}
double slope =
((toPoint.latitude - fromPoint.latitude) / (toPoint.longitude - fromPoint.longitude));
return slope;

}

/**
* 根据两点算取图标转的角度
*/

public static double getAngle(LatLng fromPoint, LatLng toPoint) {
if (fromPoint == null || toPoint == null) {
return 0;
}
double slope = getSlope(fromPoint, toPoint);
if (slope == Double.MAX_VALUE) {
if (toPoint.latitude > fromPoint.latitude) {
return 0;
} else {
return 180;
}
}
float deltAngle = 0;
if ((toPoint.latitude - fromPoint.latitude) * slope < 0) {
deltAngle = 180;
}
double radio = Math.atan(slope);
double angle = 180 * (radio / Math.PI) + deltAngle - 90;
return angle;
}

/**
* 根据点和斜率算取截距
*/

public static double getInterception(double slope, LatLng point) {
if (point == null) {
return 0;
}
return point.latitude - slope * point.longitude;
}

/**
* 计算x方向每次移动的距离
*/

public static double getXMoveDistance(double slope) {
if (slope == Double.MAX_VALUE) {
return DISTANCE;
}
return Math.abs((DISTANCE * slope) / Math.sqrt(1 + slope * slope));
}

/**
* 根据轨迹线段计算小车走了多少步
*
* @param latLngList
* @return
*/

public static int getStep(List<LatLng> latLngList) {
int step = 0;
if (latLngList != null && latLngList.size() > 1) {
for (int i = 0; i < latLngList.size() - 1; i++) {
try {
LatLng startPoint = latLngList.get(i);
LatLng endPoint = latLngList.get(i + 1);
double slope = getSlope(startPoint, endPoint);
// 是不是正向的标示(向上设为正向)
boolean isReverse = (startPoint.latitude > endPoint.latitude);
double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
// 应该对经纬度同时处理
for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
j - xMoveDistance) {
step++;
}
} catch (Exception e) {
e.printStackTrace();
}
}

}
return step;
}

/**
* 根据总距离和步数计算运动时间
*
* @param distance
* @param step
* @return
*/

public static double getMoveTime(float distance, int step) {
double timeInterval = 0;
if (distance > 0) {
float totalDistance = distance * 1000;
if (totalDistance <= 500) {
timeInterval = 1000.0 / step;
} else if (totalDistance > 500 && totalDistance <= 7500) {
timeInterval = 2.0 * totalDistance / step;
} else {
timeInterval = 15000.0 / step;
}
}
return timeInterval;
}

/**
* 根据轨迹点集合计算总距离
*
* @param latLngList
* @return
*/

public static float getDistance(List<LatLng> latLngList) {
float distance = 0;
if (latLngList != null && latLngList.size() > 1) {
for (int i = 0; i < latLngList.size() - 1; i++) {
try {
distance += AMapUtils.calculateLineDistance(latLngList.get(i), latLngList.get(i + 1));
} catch (Exception e) {
e.printStackTrace();
}
}
}
return distance;
}

// latitude - 地点的纬度,在-90 与90 之间的double 型数值。
// longitude - 地点的经度,在-180 与180 之间的double 型数值。
/**
* 根据一个经纬度字符串求一个经纬度集合a|b|c|d;
*
* @param latlonStr
* @return
*/

public static List<LatLng> getListLatLng(String latlonStr) {
if (!TextUtils.isEmpty(latlonStr)) {
String[] trackArr = latlonStr.split("\\|");
if (trackArr != null && trackArr.length > 0) {
List<LatLng> latLngList = new ArrayList<LatLng>();
for (int i = 0; i < trackArr.length - 1; i = i + 2) {
try {
String lat = trackArr[i + 1];
String lng = trackArr[i];
// Logger.v(TAG,"trackArr index:" + i);
// Logger.v(TAG,"trackArr lat:" + lat);
// Logger.v(TAG,"trackArr lng:" + lng);
if (!TextUtils.isEmpty(lat) && !TextUtils.isEmpty(lng)) {
Double dLat = Double.valueOf(lat);
Double dLng = Double.valueOf(lng);
if (dLat >= -90 && dLat <= 90 && dLng >= -180 && dLng <= 180
&& !(dLat == 0 && dLng == 0)) {
LatLng latLng = new LatLng(dLat, dLng);
latLngList.add(latLng);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return latLngList;
}
}
return null;
}
}

高德API实现的实时运动轨迹代码:

public class MoveCarSmoothThread implements IMoveCar {
public static final String TAG = MoveCarSmoothThread.class.getSimpleName();
private MovingPointOverlay mMovingPointOverlay;
private WeakReference<MainActivity> mActivityWeakReference;
private boolean isfirst = true;
private MOVE_STATE currMoveState = MOVE_STATE.START_STATUS;

public void setCurrMoveState(MOVE_STATE currMoveState) {
this.currMoveState = currMoveState;
}

public MOVE_STATE getCurrMoveState() {
return currMoveState;
}

public MoveCarSmoothThread(MainActivity activity) {
mActivityWeakReference = new WeakReference<>(activity);
}
@Override
public void startMove(List<LatLng> latLngs) {
if (latLngs == null || latLngs.size() == 0) {
return;
}

Log.v("MYTAG","startMove start:"+Thread.currentThread().getName());
Log.v(TAG, "moveCoarseTrack start.........................................................");
long startTime = System.currentTimeMillis();
Log.v(TAG, "startTime:" + startTime);
final MainActivity mainActivity = mActivityWeakReference.get();
if (mMovingPointOverlay == null) {
mMovingPointOverlay = new MovingPointOverlay(mainActivity.mAMap, mainActivity.mCarMarker);
mMovingPointOverlay.setTotalDuration(5);
mMovingPointOverlay.setMoveListener(new MovingPointOverlay.MoveListener() {
@Override
public void move(double v) {
if(isfirst){
isfirst = false;
Log.v("MYTAG","MoveCarSmoolthThread move start:"+Thread.currentThread().getName());
}

LatLng position = mMovingPointOverlay.getPosition();
mainActivity.mLatLngList.add(position);// 向轨迹集合增加轨迹点
mainActivity.mMovePolyline.setPoints(mainActivity.mLatLngList);// 轨迹画线开始

Message message = Message.obtain();
message.what = MainActivity.EventType.MapMove;
message.obj = position;
message.arg1 = (int)v;
mainActivity.mMainHandler.sendMessage(message);
}
});
}
mMovingPointOverlay.setPoints(latLngs);
mMovingPointOverlay.startSmoothMove();
long endTime = System.currentTimeMillis();
Log.v(TAG, "endTime:" + endTime);
Log.v(TAG, "moveCoarseTrack end.........................................................");
}

@Override
public void reStartMove() {
if(mMovingPointOverlay != null){
mMovingPointOverlay.startSmoothMove();
}
}
@Override
public void pauseMove(){
if(mMovingPointOverlay != null){
mMovingPointOverlay.stopMove();
}
}
@Override
public void stopMove(){
if(mMovingPointOverlay != null){
mMovingPointOverlay.destroy();
mMovingPointOverlay = null;
}
if(mActivityWeakReference.get() != null){
mActivityWeakReference.get().mLatLngList.clear();
}
}

}

最后我把整个项目的的完整代码传到GitHub上了:https://github.com/geduo83/android-amap-movecar

---end---

推荐阅读:

七夕,不能送个对象,还不能送一本书吗?

一篇文章彻底明白Android文件存储

Android仿网易云音乐鲸云音效动效

每一个“在看”,我都当成真的喜欢

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存