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:
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
- 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
}
- 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
}
III. Map Interaction
- 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();
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)