前言
在運(yùn)動(dòng)類應(yīng)用中,軌跡播放效果是提升用戶體驗(yàn)的關(guān)鍵功能之一。它不僅能直觀展示用戶的運(yùn)動(dòng)路線,還能通過動(dòng)態(tài)效果增強(qiáng)運(yùn)動(dòng)的趣味性。Keep 作為一款知名的運(yùn)動(dòng)健身應(yīng)用,其軌跡播放效果深受用戶喜愛。那么,如何在鴻蒙系統(tǒng)中開發(fā)出類似 Keep 的軌跡播放效果呢?本文將通過實(shí)際代碼案例,深入解析實(shí)現(xiàn)這一功能的關(guān)鍵步驟和技術(shù)要點(diǎn)。
效果:

一、核心功能拆解
要實(shí)現(xiàn)類似 Keep 的軌跡播放效果,我們需要完成以下幾個(gè)核心功能:
? 動(dòng)態(tài)軌跡播放:通過定時(shí)器和動(dòng)畫效果,實(shí)現(xiàn)軌跡的動(dòng)態(tài)播放,模擬用戶運(yùn)動(dòng)過程。
? 地圖交互:在地圖上繪制軌跡,并根據(jù)播放進(jìn)度更新地圖中心點(diǎn)和旋轉(zhuǎn)角度。
二、動(dòng)態(tài)軌跡播放
1.播放邏輯
通過定時(shí)器和動(dòng)畫效果實(shí)現(xiàn)軌跡的動(dòng)態(tài)播放。以下是播放軌跡的核心代碼:
private playTrack() {
// 如果已經(jīng)在播放,則停止
if (this.playTimer) {
this.mapController?.removeOverlay(this.polyline);
clearInterval(this.playTimer);
this.playTimer = undefined;
if (this.animationTimer) {
clearInterval(this.animationTimer);
}
if (this.movingMarker) {
this.mapController?.removeOverlay(this.movingMarker);
this.movingMarker = undefined;
}
this.currentPointIndex = 0;
return;
}
// 創(chuàng)建動(dòng)態(tài)位置標(biāo)記
this.movingMarker = new Marker({
position: this.trackPoints[0],
icon: new ImageEntity("rawfile://images/ic_run_detail_start.png"),
isJoinCollision: SysEnum.CollisionBehavior.NOT_COLLIDE,
located: SysEnum.Located.CENTER
});
this.mapController?.addOverlay(this.movingMarker);
// 開始播放
this.playTimer = setInterval(() => {
this.currentPointIndex++;
if (this.currentPointIndex >= this.trackPoints.length) {
clearInterval(this.playTimer);
this.playTimer = undefined;
this.currentPointIndex = 0;
if (this.movingMarker) {
this.mapController?.removeOverlay(this.movingMarker);
this.movingMarker = undefined;
}
return;
}
// 更新動(dòng)態(tài)位置標(biāo)記位置,使用setInterval實(shí)現(xiàn)平滑移動(dòng)
if (this.movingMarker && this.currentPointIndex < this.trackPoints.length - 1) {
const currentPoint = this.trackPoints[this.currentPointIndex];
const nextPoint = this.trackPoints[this.currentPointIndex + 1];
let animationProgress = 0;
// 清除之前的動(dòng)畫定時(shí)器
if (this.animationTimer) {
clearInterval(this.animationTimer);
}
// 創(chuàng)建新的動(dòng)畫定時(shí)器,每10ms更新一次位置
this.animationTimer = setInterval(() => {
animationProgress += 0.1; // 每次增加0.1的進(jìn)度
if (animationProgress >= 1) {
clearInterval(this.animationTimer);
this.animationTimer = undefined;
this.movingMarker?.setPosition(new LatLng(nextPoint.lat, nextPoint.lng));
} else {
const interpolatedLat = currentPoint.lat + (nextPoint.lat - currentPoint.lat) * animationProgress;
const interpolatedLng = currentPoint.lng + (nextPoint.lng - currentPoint.lng) * animationProgress;
this.movingMarker?.setPosition(new LatLng(interpolatedLat, interpolatedLng));
}
}, 10); // 每10ms執(zhí)行一次
}
// 繪制當(dāng)前軌跡線段
const currentPoints = this.trackPoints.slice(0, this.currentPointIndex + 1);
const currentColors = PathGradientTool.getPathColors(this.record!.points.slice(0, this.currentPointIndex + 1), 100);
if (this.polyline) {
this.mapController?.removeOverlay(this.polyline);
this.polyline.remove();
this.polyline.destroy();
}
this.polyline = new Polyline({
points: currentPoints,
width: 5,
join: SysEnum.LineJoinType.ROUND,
cap: SysEnum.LineCapType.ROUND,
isGradient: true,
colorList: currentColors!
});
this.mapController?.addOverlay(this.polyline);
// 更新地圖中心點(diǎn)和旋轉(zhuǎn)角度
let bearing = 0;
if (this.currentPointIndex < this.trackPoints.length - 1) {
const currentPoint = this.trackPoints[this.currentPointIndex];
const nextPoint = this.trackPoints[this.currentPointIndex + 1];
bearing = Math.atan2(
nextPoint.lat - currentPoint.lat,
nextPoint.lng - currentPoint.lng
) * 180 / Math.PI;
bearing = (bearing + 360) % 360;
bearing = (360 - bearing + 90) % 360;
}
this.mapController?.mapStatus.setRotate(bearing).setOverlooking(90).setCenterPoint(new LatLng(this.trackPoints[this.currentPointIndex].lat, this.trackPoints[this.currentPointIndex].lng)).refresh();
}, 100); // 每100ms移動(dòng)一次
}
2.動(dòng)畫效果
通過定時(shí)器和線性插值實(shí)現(xiàn)動(dòng)態(tài)軌跡的平滑移動(dòng)效果。以下是動(dòng)畫效果的核心代碼:
if (this.movingMarker && this.currentPointIndex < this.trackPoints.length - 1) {
const currentPoint = this.trackPoints[this.currentPointIndex];
const nextPoint = this.trackPoints[this.currentPointIndex + 1];
let animationProgress = 0;
// 清除之前的動(dòng)畫定時(shí)器
if (this.animationTimer) {
clearInterval(this.animationTimer);
}
// 創(chuàng)建新的動(dòng)畫定時(shí)器,每10ms更新一次位置
this.animationTimer = setInterval(() => {
animationProgress += 0.1; // 每次增加0.1的進(jìn)度
if (animationProgress >= 1) {
clearInterval(this.animationTimer);
this.animationTimer = undefined;
this.movingMarker?.setPosition(new LatLng(nextPoint.lat, nextPoint.lng));
} else {
const interpolatedLat = currentPoint.lat + (nextPoint.lat - currentPoint.lat) * animationProgress;
const interpolatedLng = currentPoint.lng + (nextPoint.lng - currentPoint.lng) * animationProgress;
this.movingMarker?.setPosition(new LatLng(interpolatedLat, interpolatedLng));
}
}, 10); // 每10ms執(zhí)行一次
}
三、地圖交互
1.地圖中心點(diǎn)和旋轉(zhuǎn)角度更新
在播放軌跡的過程中,動(dòng)態(tài)更新地圖的中心點(diǎn)和旋轉(zhuǎn)角度,以確保用戶始終能看到當(dāng)前播放的位置。以下是更新地圖中心點(diǎn)和旋轉(zhuǎn)角度的代碼:
let bearing = 0;
if (this.currentPointIndex < this.trackPoints.length - 1) {
const currentPoint = this.trackPoints[this.currentPointIndex];
const nextPoint = this.trackPoints[this.currentPointIndex + 1];
bearing = Math.atan2(
nextPoint.lat - currentPoint.lat,
nextPoint.lng - currentPoint.lng
) * 180 / Math.PI;
bearing = (bearing + 360) % 360;
bearing = (360 - bearing + 90) % 360;
}
this.mapController?.mapStatus.setRotate(bearing).setOverlooking(90).setCenterPoint(new LatLng(this.trackPoints[this.currentPointIndex].lat, this.trackPoints[this.currentPointIndex].lng)).refresh();
四、總結(jié)
通過上述步驟,我們成功實(shí)現(xiàn)了類似 Keep 的軌跡播放效果。不僅提升了用戶體驗(yàn),還為運(yùn)動(dòng)數(shù)據(jù)的可視化提供了有力支持。