鴻蒙運(yùn)動(dòng)開發(fā)實(shí)戰(zhàn):打造 Keep 式軌跡播放效果

前言

在運(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)。

效果:

1748337679822.gif

一、核心功能拆解

要實(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ù)的可視化提供了有力支持。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容