轨迹回放功能主要使用ValueAnimator对轨迹动态绘制。
view on github
本篇是基于LineLayer的实际使用,开发当中也确实会遇到这样的需求。
先上图
图一 图二
可以看出主要的3个元素。
1.运动的点
2.轨迹路线
3.轨迹线灰色背景(单纯的为了更好的显示效果存在)
总体思路:
一、图一实现
1.首先获取List<Point> point (轨迹点数据源)。
2.绘制红色路线与灰色背景路线。
3. 使用ValueAnimator动画遍历取出,动态更新点的位置。
二、图二实现
与图一的区别在于红色路线也是动态绘制的。
1.首先获取List<Point> point (轨迹点数据源)。
2.绘制灰色背景路线。
3. 使用ValueAnimator动画遍历取出,动态更新点的位置同时动态绘制红色路线。
直接上代码:
代码中已经尽量添加了详细的注释。
MarkerFollowingRouteActivity:
package com.ming.androidmapbox.layers;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.animation.LinearInterpolator;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.LineString;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.style.layers.LineLayer;
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import com.ming.androidmapbox.R;
import java.util.ArrayList;
import java.util.List;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineWidth;
/**
* 标记沿GeoJSON路线行驶
*/
public class MarkerFollowingRouteActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "MarkerFollowingRoute";
private static final String DOT_SOURCE_ID = "dot-source-id";
private static final String DOT_LAYER_ID = "dot-layer-id";
private static final String DOT_IMAGE_ID = "dot-image-id";
private static final String LINE_SOURCE_ID = "line-source-id";
private static final String LINE_LAYER_ID = "line-layer-id";
private static final String LINE_BG_SOURCE_ID = "line-bg-source-id";
private static final String LINE_BG_LAYER_ID = "line-bg-layer-id";
private MapView mapView;
private int count = 0;
private Handler handler;
private Runnable runnable;
private GeoJsonSource dotGeoJsonSource;
private ValueAnimator markerIconAnimator;
private LatLng markerIconCurrentLocation;
private List<Point> routeCoordinates;
private boolean isFollowMode;
private GeoJsonSource lineGeoJsonSource;
private List<Point> followRouteCoordinates = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Mapbox.getInstance(this, getString(R.string.mapbox_access_token));
setContentView(R.layout.activity_lab_marker_following_route);
mapView = findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
mapboxMap.setStyle(Style.LIGHT, new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NonNull Style style) {
initCoordinates();
initSources(style);
initBgDotLinePath(style);
initDotLinePath(style);
initDotLayer(style);
//按钮监听
findViewById(R.id.btn_route).setOnClickListener(MarkerFollowingRouteActivity.this);
findViewById(R.id.btn_route_follow).setOnClickListener(MarkerFollowingRouteActivity.this);
}
});
}
});
}
/**
* 创造轨迹点数据源,手动添加一组点
*/
private void initCoordinates() {
routeCoordinates = new ArrayList<>();
routeCoordinates.add(Point.fromLngLat(-77.044211, 38.852924));
routeCoordinates.add(Point.fromLngLat(-77.045659, 38.860158));
routeCoordinates.add(Point.fromLngLat(-77.044232, 38.862326));
routeCoordinates.add(Point.fromLngLat(-77.040879, 38.865454));
routeCoordinates.add(Point.fromLngLat(-77.039936, 38.867698));
routeCoordinates.add(Point.fromLngLat(-77.040338, 38.86943));
routeCoordinates.add(Point.fromLngLat(-77.04264, 38.872528));
routeCoordinates.add(Point.fromLngLat(-77.03696, 38.878424));
routeCoordinates.add(Point.fromLngLat(-77.032309, 38.87937));
routeCoordinates.add(Point.fromLngLat(-77.030056, 38.880945));
routeCoordinates.add(Point.fromLngLat(-77.027645, 38.881779));
routeCoordinates.add(Point.fromLngLat(-77.026946, 38.882645));
routeCoordinates.add(Point.fromLngLat(-77.026942, 38.885502));
routeCoordinates.add(Point.fromLngLat(-77.028054, 38.887449));
routeCoordinates.add(Point.fromLngLat(-77.02806, 38.892088));
routeCoordinates.add(Point.fromLngLat(-77.03364, 38.892108));
routeCoordinates.add(Point.fromLngLat(-77.033643, 38.899926));
}
/**
* 初始化各个源
* @param loadedMapStyle
*/
private void initSources(@NonNull Style loadedMapStyle) {
//根据轨迹点数据源构建FeatureCollection
FeatureCollection featureCollection = FeatureCollection.fromFeatures(
new Feature[]{Feature.fromGeometry(LineString.fromLngLats(routeCoordinates))});
//添加点的源
dotGeoJsonSource = new GeoJsonSource(DOT_SOURCE_ID, featureCollection);
loadedMapStyle.addSource(dotGeoJsonSource);
//添加红色的线的源
lineGeoJsonSource = new GeoJsonSource(LINE_SOURCE_ID, featureCollection);
loadedMapStyle.addSource(lineGeoJsonSource);
//添加灰色背景线的源
GeoJsonSource lineBgGeoJsonSource = new GeoJsonSource(LINE_BG_SOURCE_ID, featureCollection);
loadedMapStyle.addSource(lineBgGeoJsonSource);
}
/**
* 添加灰色背景线
*
* @param loadedMapStyle
*/
private void initBgDotLinePath(@NonNull Style loadedMapStyle) {
//添加灰色背景线的layer
loadedMapStyle.addLayer(new LineLayer(LINE_BG_LAYER_ID, LINE_BG_SOURCE_ID).withProperties(
lineColor(Color.GRAY),
lineWidth(10f)
));
}
/**
* 添加红色的线
*
* @param loadedMapStyle
*/
private void initDotLinePath(@NonNull Style loadedMapStyle) {
//添加红色的线的layer
loadedMapStyle.addLayer(new LineLayer(LINE_LAYER_ID, LINE_SOURCE_ID).withProperties(
lineColor(Color.parseColor("#F13C6E")),
lineWidth(4f)
));
}
/**
* 添加运动的点
*
* @param loadedMapStyle
*/
private void initDotLayer(@NonNull Style loadedMapStyle) {
//添加点的图片资源
loadedMapStyle.addImage(DOT_IMAGE_ID, getResources().getDrawable(R.drawable.pink_dot));
//添加点的layer
loadedMapStyle.addLayer(new SymbolLayer(DOT_LAYER_ID, DOT_SOURCE_ID).withProperties(
iconImage(DOT_IMAGE_ID),
iconSize(1f),
iconIgnorePlacement(true),
iconAllowOverlap(true)
));
}
@Override
public void onResume() {
super.onResume();
mapView.onResume();
// 恢复活动后,我们将重新启动标记动画。
if (handler != null && runnable != null) {
handler.post(runnable);
}
}
@Override
public void onPause() {
super.onPause();
mapView.onPause();
//检查标记当前是否正在设置动画,如果是,则我们暂停动画,以便当活动不在视图中时不使用资源。
if (handler != null && runnable != null) {
handler.removeCallbacksAndMessages(null);
}
if (markerIconAnimator != null) {
markerIconAnimator.cancel();
}
}
@Override
protected void onStart() {
super.onStart();
mapView.onStart();
}
@Override
protected void onStop() {
super.onStop();
mapView.onStop();
}
@Override
public void onLowMemory() {
super.onLowMemory();
mapView.onLowMemory();
}
@Override
protected void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
@Override
public void onClick(View v) {
//动画执行过程中不允许点击
if (markerIconAnimator != null && markerIconAnimator.isRunning()) {
return;
}
resetParams();
switch (v.getId()) {
case R.id.btn_route:
initRunnable(false);
break;
case R.id.btn_route_follow:
initRunnable(true);
break;
default:
break;
}
}
/**
* 设置重复逻辑以沿路径移动图标。
*/
private void initRunnable(final boolean isFollow) {
isFollowMode = isFollow;
// ValueAnimator用于在GeoJSON点之间移动标记,是线性完成的。该处理程序用于沿着GeoJSON点移动标记。
handler = new Handler();
runnable = new Runnable() {
@Override
public void run() {
//检查是否在列表的末尾,如果是停止处理程序。
if ((routeCoordinates.size() - 1 > count)) {
//下一个要移动的点的位置
Point nextLocation = routeCoordinates.get(count + 1);
//如果动画已开始就取消
if (markerIconAnimator != null && markerIconAnimator.isStarted()) {
markerIconCurrentLocation = (LatLng) markerIconAnimator.getAnimatedValue();
markerIconAnimator.cancel();
}
//取出第一个点
LatLng firstLocation = new LatLng(routeCoordinates.get(0).latitude(), routeCoordinates.get(0).longitude());
//构建markerIconAnimator
markerIconAnimator = ObjectAnimator
.ofObject(AnimalCons.LATLNG_EVALUATOR, count == 0 || markerIconCurrentLocation == null
? firstLocation
: markerIconCurrentLocation,
new LatLng(nextLocation.latitude(), nextLocation.longitude()))
.setDuration(500);
markerIconAnimator.setInterpolator(new LinearInterpolator());
markerIconAnimator.addUpdateListener(animatorUpdateListener);
markerIconAnimator.start();
//逻辑后再自增,保持当前点数不变。
count++;
//一旦完成,我们就需要在ValueAnimator完成后再次执行处理程序来重复整个过程。
handler.postDelayed(this, 500);
} else {
//复位
resetParams();
}
}
};
handler.post(runnable);
}
/**
* 重置参数
*/
private void resetParams() {
count = 0;
markerIconCurrentLocation = null;
isFollowMode = false;
followRouteCoordinates.clear();
}
/**
* ValueAnimator提供更新值时的监听
*/
private final ValueAnimator.AnimatorUpdateListener animatorUpdateListener =
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
LatLng animatedPosition = (LatLng) valueAnimator.getAnimatedValue();
Point point = Point.fromLngLat(
animatedPosition.getLongitude(), animatedPosition.getLatitude());
//动态更新点的位置
if (dotGeoJsonSource != null) {
dotGeoJsonSource.setGeoJson(point);
}
//如果是跟随模式,动态绘制红色路线
if (isFollowMode) {
if (lineGeoJsonSource != null) {
followRouteCoordinates.add(point);
FeatureCollection featureCollection = FeatureCollection.fromFeatures(
new Feature[]{Feature.fromGeometry(LineString.fromLngLats(followRouteCoordinates))});
lineGeoJsonSource.setGeoJson(featureCollection);
}
}
}
};
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:mapbox="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".layers.MarkerFollowingRouteActivity">
<com.mapbox.mapboxsdk.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
mapbox:mapbox_cameraTargetLat="38.875"
mapbox:mapbox_cameraTargetLng="-77.035"
mapbox:mapbox_cameraZoom="13">
<Button
android:id="@+id/btn_route"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标记沿路线行驶" />
<Button
android:id="@+id/btn_route_follow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="标记沿路线跟随" />
</com.mapbox.mapboxsdk.maps.MapView>
</FrameLayout>
圆点图片: