在网约车、外卖配送、车辆监控等场景中,几乎都离不开一个效果:地图上的小车图标沿着规划好的路线一点点前进,并且车头方向随路线转向而旋转。微信小程序的map组件虽然封装强大,但并没有现成的轨迹动画API,直接操作marker往往会出现图标瞬移或闪烁。本文将从问题根源讲起,逐步实现一个平滑、流畅的车辆轨迹移动动画。

为什么直接更新marker位置会导致动画不流畅
不少开发者最初的做法是:拿后端返回的一串经纬度点,用setData逐个更新marker的latitude和longitude。这个方案在小间隔、少点的情况下勉强可用,一旦点与点之间距离较大,小车图标就会瞬间跳到下一个位置,视觉上完全不是移动而是闪现。
根源在于两个层面。第一,真实轨迹点是离散的,两个相邻点之间可能相隔几十米甚至几百米,直接切换位置等于丢掉了中间过程;第二,小程序的setData本身有通信开销,高频调用会触发渲染队列堆积,反而导致掉帧。所以要解决的核心问题是:在两个离散点之间插入足够多的中间坐标,并控制更新频率在渲染可承受的范围内。
此外还有一个常被忽略的细节:车头朝向。marker的rotate属性单位是度,顺时针为正方向。如果不根据相邻两点计算方位角,小车直行时车头可能指向侧面,观感非常奇怪。
轨迹插值与角度计算的核心实现
平滑动画的本质是对相邻两个坐标点做线性插值。设起点A、终点B,动画进度比为ratio,则任意时刻的位置为:P = A + (B - A) * ratio。经度方向可直接线性插值,短距离下误差可以忽略;如果轨迹跨越纬度较大,可以用纬度的余弦值对经度差做修正,但在城市级轨迹回放场景中一般不需要。
车头角度可以通过Math.atan2计算相邻点的方位角并转换为度数。注意小程序marker的rotate为0时图标朝正上方即正北方向,所以如果你的车头图标是朝右绘制的,还需要减去90度的偏移量。
下面是核心的动画驱动代码,采用setInterval驱动插值,并整理了数据结构:
Page({
data: {
polyline: [],
markers: [],
carIndex: 0, // 当前所在轨迹段
step: 0, // 当前段内插值进度
totalStep: 20, // 每段切分的插值份数
points: [
{ latitude: 39.908, longitude: 116.397 },
{ latitude: 39.910, longitude: 116.400 },
{ latitude: 39.913, longitude: 116.404 },
{ latitude: 39.917, longitude: 116.408 }
]
},
onReady() {
// 初始化轨迹线与小车marker
this.setData({
polyline: [{
points: this.data.points,
color: '#1890FF',
width: 4,
arrowLine: true
}],
markers: [{
id: 1,
latitude: this.data.points[0].latitude,
longitude: this.data.points[0].longitude,
iconPath: '/images/car.png',
width: 32,
height: 32,
rotate: 0
}]
});
this.timer = setInterval(this.moveCar.bind(this), 50);
},
moveCar() {
const { points, carIndex, step, totalStep } = this.data;
if (carIndex >= points.length - 1) {
clearInterval(this.timer); // 走完全部轨迹,停止动画
return;
}
const start = points[carIndex];
const end = points[carIndex + 1];
const ratio = (step + 1) / totalStep;
// 线性插值得到当前位置
const lat = start.latitude + (end.latitude - start.latitude) * ratio;
const lng = start.longitude + (end.longitude - start.longitude) * ratio;
// 根据两点计算车头朝向(atan2返回弧度,需转为度)
const rotate = Math.atan2(
end.longitude - start.longitude,
end.latitude - start.latitude
) * 180 / Math.PI;
const nextStep = step + 1;
const nextIndex = nextStep >= totalStep ? carIndex + 1 : carIndex;
const resetStep = nextStep >= totalStep ? 0 : nextStep;
this.setData({
carIndex: nextIndex,
step: resetStep,
markers: [{
id: 1,
latitude: lat,
longitude: lng,
iconPath: '/images/car.png',
width: 32,
height: 32,
rotate: rotate
}]
});
},
onUnload() {
clearInterval(this.timer);
}
});
这段代码的关键点有三处:插值份数totalStep决定了动画细腻程度,一般每段切20到40份即可;定时器间隔50毫秒对应约20帧每秒,如果机型性能好可以降到33毫秒;角度只在进入新轨迹段时计算一次即可,插值过程中沿用同一角度,避免车头抖动。
用自定义图层与分层绘制增强视觉效果
只有一辆车移动还不够,实际产品中通常要求走过的路变浅、未走的路高亮,这就需要用到分层polyline的思路。把原始轨迹拆成已完成和未完成两条线,动画推进时不断更新已完成部分的终点坐标,视觉上就是路线被小车逐渐点亮。
具体做法是维护一个progressPoints数组,每帧把当前插值坐标push进去,然后渲染两条polyline:底色线用完整的轨迹点画成灰色,上层线用progressPoints画成蓝色。由于底层线始终不变,只更新上层线的points字段,渲染压力可控。
// 每帧动画中,在setData之前更新进度线
this.progressPoints = this.progressPoints || [this.data.points[0]];
this.progressPoints.push({ latitude: lat, longitude: lng });
this.setData({
polyline: [
// 底层:完整轨迹,灰色
{ points: this.data.points, color: '#DDDDDD', width: 4 },
// 上层:已走过的部分,高亮蓝色
{ points: this.progressPoints, color: '#1890FF', width: 4 }
],
markers: [{
id: 1,
latitude: lat,
longitude: lng,
iconPath: '/images/car.png',
width: 32,
height: 32,
rotate: rotate
}]
});
另一个进阶技巧是结合地图的自定义图层能力展示路况或区域贴图,不过在多数轨迹回放需求中,双polyline分层已经足够。需要注意的是,progressPoints数组会随帧数增长,如果轨迹很长,建议每隔若干帧做一次抽稀,只保留必要的拐点,避免setData的数据体积膨胀导致通信变慢。
性能优化与常见踩坑点
第一,尽量缩小setData的数据范围。不要把整个markers数组反复传递,如果页面上有多个marker,可以只更新车辆那个marker的属性,减少序列化开销。同理polyline也应只传变化的项,固定的底色线可以单独抽出来只在初始化时设置一次。
第二,帧率并非越高越好。setInterval设为16毫秒理论上能到60帧,但地图组件的重绘成本远高于普通视图,20帧每秒已经是地图动画的舒适区间。可以封装一个自适应逻辑:在低端机上增大totalStep减少setData次数,在高端机上降低间隔提升流畅度。
第三,图标资源要提前压缩。car.png建议使用72x72以内的PNG并保留透明通道,车头默认朝上绘制,这样rotate为0时与地图北向一致,不需要额外偏移量计算。如果需要阴影效果,直接把阴影合成进图标里,不要额外增加一个marker来模拟,否则两个元素不同步会露馅。
第四,页面卸载和隐藏时务必清理定时器。onHide中暂停动画、onUnload中清除定时器,否则后台持续setData会造成无谓的内存和CPU占用,甚至被系统判定为异常耗电。另外轨迹点来自网络接口时,注意做坐标系转换,微信地图使用火星坐标系,如果是GPS原始坐标需要转换后再绘制,否则小车会偏离道路。
总结
微信小程序中实现车辆轨迹动画的核心思路可以概括为:离散轨迹点线性插值补齐中间过程、用atan2计算车头朝向、双polyline分层呈现行进进度、合理控制setData频率并做好生命周期清理。按本文方案,在常规城市级轨迹回放场景中可以获得稳定流畅的动画效果。如果后续需要叠加实时定位,还可以在onLocationChange回调中动态追加轨迹点,让小车始终跟随真实位置前进,整体框架完全复用。