polyline基础结构与轨迹绘制
微信小程序的map组件通过polyline属性接收一个数组,每个数组元素就是一条折线。绘制运动轨迹时,先把采集到的坐标点整理成标准格式:每个点需要包含latitude和longitude两个字段,分别对应纬度和经度。WXML页面里可以只放一个map组件,通过id绑定地图实例,后续做视野调整会用到。基础配置如下:

<map
id="trackMap"
class="map"
latitude="{{center.latitude}}"
longitude="{{center.longitude}}"
scale="16"
show-location
polyline="{{polylines}}"
></map>
这里polyline绑定的是一个数组,因为map组件支持同时显示多条折线。JS里需要维护一个polylines数组,初始可以包含一条空轨迹。每个polyline对象至少要设置points、color和width,color接收十六进制字符串,width表示线条宽度,单位为px。加入borderWidth和borderColor可以让轨迹带描边,视觉上更清晰。
采集运动轨迹通常使用wx.startLocationUpdateBackground结合onLocationChange回调,但注意后台定位需要用户授权。前端拿到坐标后,直接push到轨迹点数组,再通过setData更新polylines即可。代码里要避免直接修改data中的嵌套对象,先复制数组再统一赋值。
Page({
data: {
trackPoints: [],
polylines: []
},
appendPoint(lat, lng) {
const points = this.data.trackPoints.concat([{ latitude: lat, longitude: lng }]);
const polyline = {
points: points,
color: '#3B82F6',
width: 6,
borderColor: '#FFFFFF',
borderWidth: 2
};
this.setData({
trackPoints: points,
polylines: [polyline]
});
}
});
上面这段只是最基础的单色轨迹。当点位不断增加时,每次setData都会把整条polyline重新提交给渲染层,点少的时候没问题,但长距离运动可能积累上千个点,渲染压力会明显上升。为了优化,可以先对轨迹点做距离过滤,例如相邻点距离小于2米就丢弃,这样能在不损失太多形状的前提下大幅减少点数。
colorList实现渐变色线条
要让轨迹呈现渐变效果,需要用到polyline的colorList属性。这个属性从基础库2.3.0开始支持,它接收一个数组,每一项包含offset和color。offset是0到1之间的小数,表示该颜色在整条折线上的起点比例,0代表起点,1代表终点。多个颜色节点之间会自动平滑过渡。例如从起点绿色过渡到中段黄色再到终点红色,可以这样定义:
const polyline = {
points: this.data.trackPoints,
width: 6,
color: '#22C55E',
colorList: [
{ offset: 0, color: '#22C55E' },
{ offset: 0.5, color: '#FACC15' },
{ offset: 1, color: '#EF4444' }
]
};
需要注意两点:offset必须严格递增,且不能重复;如果同时设置了color和colorList,colorList优先,color只作为不支持colorList时的降级方案。实际开发中建议两个属性都写上,color放在前面,colorList放在后面,这样老版本基础库会忽略不认识的colorList,仍然展示单色线条。
如果只是让轨迹从绿色均匀渐变到红色,上面的固定数组就够了。但运动场景通常会希望根据实时速度、心率或海拔来映射颜色。做法是先计算每个轨迹点到起点之间的累计距离,再用累计距离除以总距离得到offset,最后根据该点的数值插值出颜色。比如按速度区间分配颜色:速度慢为蓝色,中速为绿色,快速为红色。这里给出一个按距离均匀生成蓝绿红渐变的计算函数,它会把整条轨迹分成若干段并生成colorList。
function buildColorList(points) {
if (!points || points.length === 0) return [];
const distances = [0];
let total = 0;
for (let i = 1; i < points.length; i++) {
const d = getDistance(points[i - 1], points[i]);
total += d;
distances.push(total);
}
const colorList = [];
const stops = [
{ offset: 0, color: '#3B82F6' },
{ offset: 0.5, color: '#22C55E' },
{ offset: 1, color: '#EF4444' }
];
for (let i = 0; i < points.length; i++) {
const ratio = total === 0 ? 0 : distances[i] / total;
colorList.push({ offset: ratio, color: interpolateColor(stops, ratio) });
}
return colorList;
}
function getDistance(a, b) {
const R = 6371000;
const rad = Math.PI / 180;
const dLat = (b.latitude - a.latitude) * rad;
const dLng = (b.longitude - a.longitude) * rad;
const lat1 = a.latitude * rad;
const lat2 = b.latitude * rad;
const h = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
return 2 * R * Math.asin(Math.sqrt(h));
}
interpolateColor函数负责在两个颜色节点之间做线性插值,输入一个offset比例,输出十六进制颜色字符串。需要注意的是,colorList里的节点数量并不是越多越好,几百个节点虽然过渡更细腻,但会增加额外的解析开销。通常按50到100个关键点来生成已经足够平滑。
动态轨迹回放与地图视野控制
运动结束后如果希望用户看到轨迹逐步画出来,可以用定时器动态截取points。核心思路是维护一个当前回放索引,每间隔一段时间把已播放的点位追加到显示数组,并同步更新polyline。这个显示数组可以和完整轨迹数组分开,避免频繁操作原始数据。
回放过程中如果同时使用colorList,需要保证colorList的offset始终基于当前显示的部分重新计算,否则颜色比例会对应整条轨迹,而不是当前已经出现的那一段。因此每次更新时最好是重新生成一次polyline对象,带上当前points和对应的colorList。这种计算量在小程序里可以接受,只要总点数不是特别大。
startReplay() {
const allPoints = this.data.trackPoints;
const total = allPoints.length;
let cursor = 0;
this.replayTimer = setInterval(() => {
cursor = Math.min(cursor + 3, total);
const currentPoints = allPoints.slice(0, cursor);
const colorList = buildColorList(currentPoints);
const polyline = {
points: currentPoints,
width: 6,
color: '#3B82F6',
colorList: colorList
};
this.setData({ polylines: [polyline] });
this.fitMap(currentPoints);
if (cursor >= total) {
clearInterval(this.replayTimer);
}
}, 50);
},
fitMap(points) {
const mapCtx = wx.createMapContext('trackMap', this);
mapCtx.includePoints({
points: points,
padding: [60, 40, 60, 40]
});
}
代码中的定时器步长设为3个点,如果50毫秒执行一次,一秒能推进约60个点,回放速度相对自然。步长和间隔可以根据点密度调整,点密时可以一次推进更多点。地图视野用includePoints动态框住当前已回放的点,padding参数让轨迹不要贴边,单位是px,数组顺序为上右下左。需要注意频繁调用includePoints也会产生性能消耗,可以每隔几帧调用一次,或者设置一个节流变量。
如果回放过程中需要支持暂停和继续,只需要保留cursor变量和定时器句柄,暂停时clearInterval,继续时重新启动即可。页面卸载时务必清理定时器,避免后台运行造成内存泄漏。另外真机上如果把scale设置得很小,轨迹展示会比较平缓,可以先根据总点位计算合适的初始缩放层级。
轨迹点抽稀与兼容性处理
长时间的户外运动可能采集到几千甚至上万个坐标点。如果全部交给polyline渲染,setData耗时和地图渲染都会明显增加,滑动地图时也可能掉帧。一种简单有效的做法是使用Douglas-Peucker算法对轨迹点做抽稀,保留关键拐点,丢弃近似直线上的冗余点。算法基于点到线段的垂直距离判断是否保留,可以在离线阶段执行,也可以在采集过程中定期压缩队列。
function simplify(points, tolerance) {
if (points.length <= 2) return points;
const sqTolerance = tolerance * tolerance;
function step(first, last) {
let maxSqDist = 0;
let index = 0;
for (let i = first + 1; i < last; i++) {
const d = perpendicularSqDist(points[i], points[first], points[last]);
if (d > maxSqDist) {
maxSqDist = d;
index = i;
}
}
if (maxSqDist > sqTolerance) {
const left = step(first, index);
const right = step(index, last);
return left.slice(0, left.length - 1).concat(right);
}
return [points[first], points[last]];
}
return step(0, points.length - 1);
}
上述函数中的perpendicularSqDist需要自行实现,用来计算点到线段的最短距离平方。抽稀阈值一般根据设备定位精度来定,比如步行可以设5米,骑行设8米,驾驶可以设15米。阈值越大,轨迹越简化,但也越可能丢掉细小弯道。除了抽稀,还可以对连续点做中值滤波,把明显跳动的GPS漂移点剔除。
colorList的基础库兼容性问题也需要在发布前确认。可以在小程序后台查看运行用户的基础库分布,如果仍有较多样本低于2.3.0,最好保留单色降级逻辑。判断基础库版本可以用wx.getSystemInfoSync().SDKVersion和比较函数。另一个细节是colorList中的color格式只支持十六进制,不支持rgba或命名颜色,写入前必须先转换。