DEV Community

zhonghua
zhonghua

Posted on • Edited on

Practical HarmonyOS Sports Development: Creating a Keep-Style Trajectory Playback Effect

Practical HarmonyOS Sports Development: Creating a Keep-Style Trajectory Playback Effect

Foreword

In sports applications, the trajectory playback effect is one of the key features to enhance user experience. It not only intuitively displays the user's sports route but also enhances the fun of sports through dynamic effects. Keep, as a well-known sports and fitness application, has a popular trajectory playback effect. So, how can we develop a similar Keep-style trajectory playback effect in the HarmonyOS system? This article will delve into the key steps and technical points to achieve this functionality through practical code examples.

Effect:

Image description

I. Core Function Decomposition

To achieve a Keep-style trajectory playback effect, we need to complete the following core functions:

  • Dynamic Trajectory Playback: Implement dynamic trajectory playback through timers and animation effects to simulate the user's sports process.

  • Map Interaction: Draw the trajectory on the map and update the map center point and rotation angle according to the playback progress.

II. Dynamic Trajectory Playback

  1. Playback Logic

Implement dynamic trajectory playback through timers and animation effects. Below is the core code for playing the trajectory:

private playTrack() {
  // If already playing, stop
  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;
  }

  // Create a dynamic position marker
  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);

  // Start playback
  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;
    }

    // Update the dynamic position marker position, using setInterval to achieve smooth movement
    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;

      // Clear the previous animation timer
      if (this.animationTimer) {
        clearInterval(this.animationTimer);
      }

      // Create a new animation timer, updating the position every 10ms
      this.animationTimer = setInterval(() => {
        animationProgress += 0.1; // Increase the progress by 0.1 each time

        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); // Execute every 10ms
    }

    // Draw the current trajectory segment
    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);

    // Update the map center point and rotation angle
    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); // Move every 100ms
}
Enter fullscreen mode Exit fullscreen mode
  1. Animation Effect

Implement the smooth movement effect of the dynamic trajectory through timers and linear interpolation. Below is the core code for the animation effect:

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;

  // Clear the previous animation timer
  if (this.animationTimer) {
    clearInterval(this.animationTimer);
  }

  // Create a new animation timer, updating the position every 10ms
  this.animationTimer = setInterval(() => {
    animationProgress += 0.1; // Increase the progress by 0.1 each time

    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); // Execute every 10ms
}
Enter fullscreen mode Exit fullscreen mode

III. Map Interaction

  1. Updating Map Center Point and Rotation Angle

During the playback of the trajectory, dynamically update the map center point and rotation angle to ensure that the user can always see the current playback location. Below is the code to update the map center point and rotation angle:

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();
Enter fullscreen mode Exit fullscreen mode

IV. Summary

Through the above steps, we have successfully implemented a Keep-style trajectory playback effect. This not only enhances the user experience but also provides strong support for the visualization of sports data.

Top comments (0)