版权声明:本文来自门心叼龙的博客,属于原创内容,转载请注明出处:https://menxindiaolong.blog.csdn.net/article/details/95789128

github源码下载地址:https://github.com/geduo83/android-amap-movecar

关于车辆运动的相关文章一共写过两篇,一篇为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;}@Overridepublic 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;}} elseSystemClock.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线程,内部包含LooperHandlerThread handlerThread = new HandlerThread("gityuan.com");handlerThread.start();// Step 2: 创建HandlerHandler handler = new Handler(handlerThread.getLooper()) {public void handleMessage(Message msg) {// process incoming messages here}};// Step 3: 发送消息handler.post(new Runnable() {@Overridepublic void run() {System.out.println("thread id="+Thread.currentThread().getId());}});

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

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

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

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

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

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

完整的代码如下:

/*** Description: <MoveCarCustomThread><br>* Author:      mxdl<br>* Date:        2019/7/10<br>* Version:     V1.0.0<br>* Update:     <br>*/
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;}@Overridepublic void run() {super.run();//设置该线程为loop线程Looper.prepare();moveCarHandler = new Handler() {@Overridepublic 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实现的实时运动轨迹

/*** Description: <MoveCarCustomThread><br>* Author:      mxdl<br>* Date:        2019/7/10<br>* Version:     V1.0.0<br>* Update:     <br>*/
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);}@Overridepublic 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() {@Overridepublic 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.........................................................");}@Overridepublic void reStartMove() {if(mMovingPointOverlay != null){mMovingPointOverlay.startSmoothMove();}}@Overridepublic void pauseMove(){if(mMovingPointOverlay != null){mMovingPointOverlay.stopMove();}}@Overridepublic 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

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

  1. android double精度_Android车辆运动轨迹平滑移动(高仿滴滴打车)最佳实践

    点击上方"Android技术杂货铺",选择"标星" 干货文章,第一时间送达! 作者:门心叼龙 链接:https://www.jianshu.com/p/015b ...

  2. Flutter高仿微信-项目实践59篇

    Flutter高仿微信(支持Android和IOS系统) Flutter高仿微信主要包含5大模块: 1.Web服务器 2.Flutter客户端 3.Xmpp即时通讯服务器 4.视频通话服务器 5.腾讯 ...

  3. Kotlin高仿微信-项目实践58篇

    Kotlin高仿微信项目实践主要包含5大模块: 1.Web服务器 2.Kotlin客户端 3.Xmpp即时通讯服务器 4.视频通话服务器 5.腾讯云服务器 另外也有Flutter版本高仿微信功能,Fl ...

  4. 全链路压测构建高可用应用最佳实践

    简介:电商大促.明星直播.在线教育等大量场景,用户访问量会在一定时间段剧增,或者在业务上云过程中需要高可用体系评估客户系统在云环境下面的性能表现,判断是否符合客户的预期. 通过阿里云的PTS+AHAS ...

  5. android 上下滚动文字_android高仿今日头条富文本编辑(发布文章)

    前言 在经历了几个月的项目期限.我们遇到了前端发布文章,要用到富文本编辑的功能.在一番衡量下最终用到了[richeditor-android](https://github.com/wasabeef/ ...

  6. android弹窗使用总结,高仿QQ空间操作弹窗

    android弹窗一共有两种方式,一种是dialog及其子类,另一种是popupwindow:Dialog及其子类尤其AlertDialog是最常用的,也是最自由的一种. **Popupwindow与 ...

  7. Android ActionBar应用实战,高仿微信主界面的设计

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/26365683 经过前面两篇文章的学习,我想大家对ActionBar都已经有一个相对 ...

  8. android抖音loading动画,高仿抖音视频加载动画

    动画效果: loadingView.gif 由于GIF图压缩的问题看起来不好看,在真机和模拟器上看是没有问题的 这个动画实现起来还是很容易的,原理其实就是利用CGAffineTransformScal ...

  9. Android 应用开发(19)--- 应用权限最佳实践

    应用权限最佳实践 权限请求保护设备提供的敏感信息,并且只有在访问信息对于您的应用的运行是必要的时才应使用.本文档提供了有关如何在不需要访问此类信息的情况下实现相同(或更好)功能的提示; 这并不是关于权 ...

最新文章

  1. Android Studio添加文件注释头模板?
  2. 万物皆可JOJO:这个GAN直接让马斯克不做人啦 | Demo可玩
  3. tensorboard运行
  4. 远程桌面无法连接服务器,启动Terminal Services 服务报1053错误
  5. OpenCV | 图片与视频的相互转换(C++Python)
  6. AndroidStudio_开发工具_调试功能---Android原生开发工作笔记75
  7. 检测到在集成的托管管道模式下不适用的ASP.NET设置的解决方法(非简单设置为【经典】模式)。 - CatcherX...
  8. 《我在谷歌大脑见习机器学习的一年:Node.js创始人的尝试笔记》阅读笔记
  9. 50行python代码自动生成文章_如何通过50行Python代码获取公众号全部文章
  10. 如何用阿里云服务器建立一个wordpress网站
  11. 正确的座机号码格式_电话号码格式怎么输入才是正确
  12. python除法运算定律有哪些_运算定律有哪些
  13. 怎么禁用笔记本的键盘
  14. 人机交互-13-复习总览
  15. 【转】用 Go 构建一个区块链
  16. Objective-C和iPHONE系列教程
  17. [ZJOI2005]沼泽鳄鱼
  18. 易开发易投产的51单片机时钟计时器:番茄时钟——TFT彩屏显示方案
  19. 「Luogu4363/BZOJ5248」[九省联考2018]一双木棋chess
  20. qq浏览器打开word 技术原理_实操:QQ群排名技术原理

热门文章

  1. java开发中JDBC连接MySQL
  2. 算法(7)分支限界法
  3. 2385. 感染二叉树需要的总时间
  4. Eclipse 运行web项目 HTTP404错误
  5. win10 设置共享文件
  6. 今日世界的战国时代格局:千年的轮回,惊人的相似?
  7. Unity制作二次元卡通渲染角色材质——1、资源分析
  8. Linux模块编程-Linux(17)
  9. SAP固定资产计提折旧费到内部订单
  10. 【HDL系列】乘法器(1)——乘累加乘法器