先讲一下思路,再贴代码
效果
车辆的图片比较难找,最后随便找张图片
背景
公司说要实现一个车辆监控功能,所以先给了我一些经纬度,叫我模拟车辆移动的过程.
刚开始实现的方式就是简单的将车辆不断的设置到下一个经纬度,但这种实现方式有一个问题,就是将地图拉大的时候车辆看起来明显不是在移动,是在跳到某个点.
然后看到web端的效果是真正的在移动,但不断地查百度的api都没有可以让marker移动的方法.
实现思路
最后想出一种方式,就是将2个点分解了n多个点,让车辆更加频繁的跳,从而实现类平滑移动.然后看到百度地图最大可以放大到5m/cm,
所以决定设置最大方法20m/cm,然后2点之间每个5米移动一次,也就是如果将地图缩放到最大,车辆每次移动的距离是0.25cm,这个时候肉眼看起来就比较像在移动
最后经过测试,当手机有点卡的时候,看起来还是有点假.当手机比较流畅的时候,看起来就真的是在移动.当然,这个睡眠时间和移动距离还是要根据实际情况进行调整
关于画线
如果按照最简单的方式,每2个点画一条线,你会发现走不到一半整个地图就卡得要死.
原因是画的线太多了,本身就已经给了200多个点,然后还要将这200多个点每2个点分成n多个点,这个时候至少都要有1,2千个点吧,所以要转换一下实现思路.
最后想到的办法是:当车辆在2个点中的点行驶的时候,不断的画线.当行驶完成后,再从第一个点和中间经过的点画一条线,再把这些小线清除掉.这样地图上就只保留一条线,也就不卡了.
可能表达得有问题,有人看不懂.举例:第一个点和第二个点之间相距20米,这个时候车辆在走完这段距离的时候会画4条线,当走玩之后就在第一个点和第二个点上画一条线,再清除这4条小线.
第二个点到第三个点:同理,中间画4条小线.最后,在百度地图上完整的线就是,第一个点到第二个点,第二个点到第三个点的线,再清除小线.这样就看起来很像边走边画
实现代码
经纬度请自行提供
class CarMoveActivity : BaseActivity(), View.OnClickListener {
private val TAG = "CarMoveActivityMsg"
private lateinit var mBaiduMap: BaiduMap
private lateinit var locationClient: LocationClient
private val MOVE_DISTANCE = 5
private val SLEEP_TIME = 20L
private lateinit var mLastMarker: Marker
private lateinit var mLastLatLng: LatLng
private var mCurrentPoint: LatLng? = null
private var mLastLine: Polyline? = null
private lateinit var mCarIcon: BitmapDescriptor
private var isStop = false
private var isPause = false
private var isRunning = false
private var mFirstIndex = 0
private var mSecondIndex = 0
private val MAX_LEVEL = 19F
private val MIN_LEVEL = 3F
override fun setContentView() {
SDKInitializer.initialize(application)
setContentView(R.layout.activity_car_move)
}
override fun initView() {
mBaiduMap = car_move_map.map
mBaiduMap.setMaxAndMinZoomLevel(MAX_LEVEL, MIN_LEVEL)
ViewUtil.setOnClick(this, car_move_start, car_move_stop, car_move_pause, cae_move_tocar)
}
override fun initData() {
setLocationOption()
mCarIcon = BitmapDescriptorFactory.fromResource(R.mipmap.car_icon3)
}
override fun onClick(v: View) {
when (v.id) {
R.id.car_move_start -> {
carMove()
}
R.id.car_move_stop -> {
//之所以使用判断而不使用,async的cancel方法
//是因为使用boolean可以控制代码执行流程,使用cancel有可能取消的时候某写关键代码只执行了
isStop = true
isRunning = false
async {
//有可能会重新赋值,所以先睡一会再赋值
Thread.sleep(100)
mFirstIndex = 0
mSecondIndex = 0
}
}
R.id.car_move_pause -> {
isStop = true
isPause = true
isRunning = false
}
R.id.cae_move_tocar -> {
if (mCurrentPoint != null) {
val u = MapStatusUpdateFactory.newLatLng(mCurrentPoint)
mBaiduMap.animateMapStatus(u)
}
}
}
}
private fun setLocationOption() {
locationClient = LocationClient(this) // 实例化LocationClient类
locationClient.registerLocationListener(bdLocationListener) // 注册监听函数
val option = LocationClientOption()
option.locationMode = LocationClientOption.LocationMode.Hight_Accuracy//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
option.setCoorType("bd09ll")//可选,默认gcj02,设置返回的定位结果坐标系
option.setScanSpan(0)//可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
option.setIsNeedAddress(true)//可选,设置是否需要地址信息,默认不需要
option.isOpenGps = true//可选,默认false,设置是否使用gps
option.isLocationNotify = true//可选,默认false,设置是否当gps有效时按照1S1次频率输出GPS结果
option.setIsNeedLocationDescribe(true)//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
option.setIsNeedLocationPoiList(true)//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
option.setIgnoreKillProcess(false)//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
option.SetIgnoreCacheException(false)//可选,默认false,设置是否收集CRASH信息,默认收集
option.setEnableSimulateGps(false)//可选,默认false,设置是否需要过滤gps仿真结果,默认需要
locationClient.locOption = option
locationClient.start()
}
private val bdLocationListener = BDLocationListener {
with(it) {
JLog.v(TAG, "mCurrentLatLng,lat:$latitude,lng:$longitude")
val u = MapStatusUpdateFactory.newLatLng(LatLng(latitude, longitude))
mBaiduMap.animateMapStatus(u)
locationClient.stop()
}
}
override fun onDestroy() {
super.onDestroy()
mCarIcon.recycle()
}
private var list1 = LatLngUtil.getList()
private var list = ArrayList<ArrayList<LatLng>>()
private fun carMove() = async {
if (isRunning) {
return@async
}
isRunning = true
isStop = true
if (mFirstIndex == 0 && mSecondIndex == 0) {
list1 = subLatLng4Distance(list1, 20)
list = splitLatLng4MoveDistance(list1)
mBaiduMap.clear()
val ood = MarkerOptions().anchor(0.5f, 0.5f).icon(mCarIcon).position(list1[0]).rotate(rotation(list1[0], list1[1]))
mLastMarker = mBaiduMap.addOverlay(ood) as Marker
mLastLatLng = list1[0]
}
isStop = false
//由于list和list1的长度一样,所以用哪个size都一样
//为什么要+1,看下面的代码再理解一下就知道了,解释起来有点麻烦,反正就是为了暂停功能
for (i in mFirstIndex + 1 until list1.size) {
if (isStop) {
return@async
}
//下面还有一个sleep,当暂时后继续的时候,会重新执行这里,如果这里也sleep的画就会sleep2次
if (isPause) {
isPause = false
} else {
Thread.sleep(SLEEP_TIME)
}
moveCar(list[i], i)
//也可以不用另外写一个方法,这样这里就不用重复判断,导致for降低效率
if (isStop) {
return@async
}
//上面的until也可以设置为size-1,这样就不用在这里做判断
//可以去for外面判断,但一开始没多想这样写,现在也懒得改了
if (i != list1.size - 1) {
mLastMarker.rotate = rotation(list1[i], list1[i + 1])
}
mFirstIndex = i
mSecondIndex = 0
}
//能执行到这里就代表车辆已经行驶完成,所以让这2个index变成0
mFirstIndex = 0
mSecondIndex = 0
isRunning = false
}
private val LINE_COLOR = 0x5500ff00.toInt()
private val Z_INDEX = -100
private var mLastSmallLine: Polyline? = null
private var mLastIndex = -1
private fun moveCar(list: ArrayList<LatLng>, index: Int) {
var firstLine: Polyline? = null
//当index不等于0的时候代表这段代码已经执行过了,不能重复执行
if (mSecondIndex == 0) {
mLastMarker.position = list[0]
//将上一个位置和当前位置画一条小线
val line = PolylineOptions().width(10).color(LINE_COLOR).points(arrayListOf(mLastLatLng, list[0])).zIndex(Z_INDEX)
mLastLatLng = list[0]
mSecondIndex = 0
firstLine = mBaiduMap.addOverlay(line) as Polyline
}
//如果该list只有一个经纬度,就没必要继续执行下去
if (list.size == 1) {
val latLngList = ArrayList<LatLng>()
(0..index).mapTo(latLngList) { list1[it] }
//画一条开始位置到现在的位置的线
val line = PolylineOptions().width(10).color(LINE_COLOR).points(latLngList).zIndex(Z_INDEX)
val tempLine = mBaiduMap.addOverlay(line) as Polyline
//清除小线和到index-1的线
firstLine?.remove()
mLastLine?.remove()
mLastLine = tempLine
return
}
//记录一条长线中的每跳小线
val lineList = ArrayList<Polyline>()
if (firstLine != null) {
lineList.add(firstLine)
}
//因为执行这个方法的时候,随时都有可能按下停止/暂停按钮,所以用finally
try {
for (i in mSecondIndex + 1 until list.size) {
if (isStop) {
return
}
Thread.sleep(SLEEP_TIME)
//画每一段小线,并记录
val line = PolylineOptions().width(10).color(LINE_COLOR).points(arrayListOf(mLastLatLng, list[i])).zIndex(Z_INDEX)
mLastMarker.position = list[i]
mLastLatLng = list[i]
mSecondIndex = i
mCurrentPoint = list[i]
lineList.add(mBaiduMap.addOverlay(line) as Polyline)
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
mLastSmallLine?.remove()
//当不是暂停/停止的的时候
if (!isStop) {
val latLngList = ArrayList<LatLng>()
(0..index).mapTo(latLngList) { list1[it] }
//绘制开始位置到现在位置的线
val line = PolylineOptions().width(10).color(LINE_COLOR).points(latLngList).zIndex(Z_INDEX)
val tempLine = mBaiduMap.addOverlay(line) as Polyline
//清除所有小线和index-1的线
lineList.map { it.remove() }
mLastLine?.remove()
mLastLine = tempLine
//一条直线画完之后必须置为空
mLastSmallLine = null
} else {
lineList.map { it.remove() }
val size = lineList.size
val line: PolylineOptions
if (mLastSmallLine == null) {//当第一次进来的时候
//从开始位置到当前位置画一条线
line = PolylineOptions().width(10).color(LINE_COLOR).points(arrayListOf(lineList[0].points[0], lineList[size - 1].points[1])).zIndex(Z_INDEX)
} else {//当第二次进来的时候
//从线的开始位置到当前位置
line = PolylineOptions().width(10).color(LINE_COLOR).points(arrayListOf(mLastSmallLine!!.points[0], lineList[size - 1].points[1])).zIndex(Z_INDEX)
}
mLastSmallLine = mBaiduMap.addOverlay(line) as Polyline
}
mLastIndex = index
}
}
private infix fun LatLng.add(latLng: LatLng): LatLng {
val ll = LatLng(latitude + latLng.latitude, longitude + latLng.longitude)
return ll
}
private infix fun LatLng.sub(latLng: LatLng): LatLng {
val ll = LatLng(latitude - latLng.latitude, longitude - latLng.longitude)
return ll
}
private infix fun LatLng.mul(count: Int): LatLng {
val ll = LatLng(latitude * count, longitude * count)
return ll
}
private infix fun LatLng.div(count: Int): LatLng {
val ll = LatLng(latitude / count, longitude / count)
return ll
}
//将一堆经纬度里面一些距离过近的经纬度去除,否则一些距离过近又不同方向的经纬度就会看来像再原地打转
private fun subLatLng4Distance(list1: ArrayList<LatLng>, distance: Int): ArrayList<LatLng> {
if (list1.size <= 1) {
return list1
}
val list = ArrayList<LatLng>()
list.add(list1[0])
(1 until list1.size)
.filter { DistanceUtil.getDistance(list1[it - 1], list1[it]) >= distance }
.mapTo(list) { list1[it] }
return list
}
//将2个点分解成n多个点
private fun splitLatLng4MoveDistance(list1: ArrayList<LatLng>): ArrayList<ArrayList<LatLng>> {
val list = ArrayList<ArrayList<LatLng>>()
list.add(arrayListOf(list1[0]))
for (i in 1 until list1.size) {
val distance = DistanceUtil.getDistance(list1[i - 1], list1[i]).toInt()
if (distance <= MOVE_DISTANCE) {
list.add(arrayListOf(list1[i]))
} else {
//当距离和要的距离取模等于0的时候必须减1,因为最后一个点必须为第二个经纬度
val count = distance / MOVE_DISTANCE - if (distance % MOVE_DISTANCE == 0) 1 else 0
val subLatLng = list1[i] sub list1[i - 1]
val divLatLng = subLatLng div count
val list2 = ArrayList<LatLng>()
repeat(count) {
val ll = list1[i - 1] add (divLatLng mul (it + 1))
list2.add(ll)
}
list2.add(list1[i])
list.add(list2)
}
}
return list
}
//车辆旋转
private fun rotation(start: LatLng, end: LatLng): Float =
if (end.longitude != start.longitude) {
val tan = (end.latitude - start.latitude) / (end.longitude - start.longitude)
val atan = Math.atan(tan)
var deg = atan * 360 / (2 * Math.PI)
deg = if (end.longitude < start.longitude) {
-deg + 90 + 90
} else {
-deg
}
-deg.toFloat()
/*deg = -deg
deg.toFloat()*/
} else {
val disy = end.latitude - start.latitude
var bias = 1
if (disy > 0) {
bias = -1
}
-bias * 90f
/*val deg = -bias * 90
deg.toFloat()*/
}
}
xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<Button
android:id="@+id/car_move_start"
android:text="start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/car_move_stop"
android:text="stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/car_move_pause"
android:text="pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/cae_move_tocar"
android:text="tocar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<com.baidu.mapapi.map.MapView
android:id="@+id/car_move_map"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
关于车辆旋转,在百度api里面查了好久都没有查到关于计算2个点的角度的方法,网上和百度开发者论坛又查不到,数学知识也忘光了,最后从一份js代码里面翻译过来
ViewUtil
class ViewUtil{
companion object {
fun setOnClick(onclickImpl : View.OnClickListener,vararg param : View) = param.forEach { it.setOnClickListener(onclickImpl) }
}
}
BaseActivity
abstract class BaseActivity :AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView()
initView()
initData()
}
protected abstract fun setContentView()
protected abstract fun initView()
protected abstract fun initData()
}
源码
http://download.csdn.net/download/android_upl/10134865