Google 地图 - 制作类似 grab app 的动画来显示路径方向
Google Maps - making an animation like grab app to show the path direction
我想制作一个沿路径移动标记的动画。
所以我考虑了如何使用比路径线稍暗的标记来做到这一点。并非常缓慢地移除和更新其位置,以便它看起来像是在移动。但我只是不认为那是视频中发生的事情。因为市场完全呈现出路径的形状。它完美地流动。
这是我目前的情况:
fun showLineAtUsersLocation(loc_destination: LatLng) {
val currentLoc = activity.getCachedCurrentLoc()
val pattern = Arrays.asList(Dash(), Gap(convertDpToPixel(6).toFloat()))
val polyLineOptions: PolylineOptions = PolylineOptions()
.add(currentLoc)
.add(loc_destination)
.geodesic(true)
.pattern(pattern)
.width(convertDpToPixel(8).toFloat())
googleMap.addPolyline(polyLineOptions)
}
但这只是显示了一条从一点到另一点的叠加线。没有真正遵循路径。我应该寻找什么?
我看到 ios 有一个 class good for this GMSStyleSpan 来处理图像。但我在 android 中找不到等效项。我没有看到跨度 class 或样式 class 我可以参考这方面的任何想法吗?或者甚至在标记上使用 AnomatedVectorDrawable
对于此类(实际上是任何类型的)动画,您可以使用 View Canvas 动画。此方法需要 MapView
-based custom view,实现:
在 MapView 上绘图 canvas;
自定义线条样式(圆而不是简单的线);
绑定到 Lat/Lon 地图坐标的路径
执行动画。
在 MapView 上绘制需要覆盖 dispatchDraw()
。自定义线条样式需要 setPathEffect()
method of Paint
class 允许为 "circle stamp"(以像素为单位)创建创建路径,这将重复每个 "advance"(也以像素为单位),类似这样:
mCircleStampPath = new Path();
mCircleStampPath.addCircle(0,0, CIRCLE_RADIUS, Path.Direction.CCW);
mCircleStampPath.close();
要将屏幕上的路径绑定到 Lat/Lon 坐标,可以使用 Projection.toScreenLocation()
needed, that requires GoogleMap
object so custom view should implements OnMapReadyCallback
for receive it. For continuous animation postInvalidateDelayed()
。因此,使用基于 MapView
的自定义 EnhancedMapView
:
的完整源代码
public class EnhancedMapView extends MapView implements OnMapReadyCallback {
private static final float CIRCLE_RADIUS = 10;
private static final float CIRCLE_ADVANCE = 3.5f * CIRCLE_RADIUS; // spacing between each circle stamp
private static final int FRAMES_PER_SECOND = 30;
private OnMapReadyCallback mMapReadyCallback;
private GoogleMap mGoogleMap;
private LatLng mPointA;
private LatLng mPointB;
private float mCirclePhase = 0; // amount to offset before the first circle is stamped
private Path mCircleStampPath;
private Paint mPaintLine;
private final Path mPathFromAtoB = new Path();
public EnhancedMapView(@NonNull Context context) {
super(context);
init();
}
public EnhancedMapView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public EnhancedMapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public EnhancedMapView(@NonNull Context context, @Nullable GoogleMapOptions options) {
super(context, options);
init();
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
canvas.save();
drawLineFomAtoB(canvas);
canvas.restore();
// perform continuous animation
postInvalidateDelayed(1000 / FRAMES_PER_SECOND);
}
private void drawLineFomAtoB(Canvas canvas) {
if (mGoogleMap == null || mPointA == null || mPointB == null) {
return;
}
final Projection mapProjection = mGoogleMap.getProjection();
final Point pointA = mapProjection.toScreenLocation(mPointA);
final Point pointB = mapProjection.toScreenLocation(mPointB);
mPathFromAtoB.rewind();
mPathFromAtoB.moveTo(pointB.x, pointB.y);
mPathFromAtoB.lineTo(pointA.x, pointA.y);
// change phase for circles shift
mCirclePhase = (mCirclePhase < CIRCLE_ADVANCE)
? mCirclePhase + 1.0f
: 0;
mPaintLine.setPathEffect(new PathDashPathEffect(mCircleStampPath, CIRCLE_ADVANCE, mCirclePhase, PathDashPathEffect.Style.ROTATE));
canvas.drawPath(mPathFromAtoB, mPaintLine);
}
private void init() {
setWillNotDraw(false);
mCircleStampPath = new Path();
mCircleStampPath.addCircle(0,0, CIRCLE_RADIUS, Path.Direction.CCW);
mCircleStampPath.close();
mPaintLine = new Paint();
mPaintLine.setColor(Color.BLACK);
mPaintLine.setStrokeWidth(1);
mPaintLine.setStyle(Paint.Style.STROKE);
mPaintLine.setPathEffect(new PathDashPathEffect(mCircleStampPath, CIRCLE_ADVANCE, mCirclePhase, PathDashPathEffect.Style.ROTATE));
postInvalidate();
}
@Override
public void getMapAsync(OnMapReadyCallback callback) {
mMapReadyCallback = callback;
super.getMapAsync(this);
}
@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
mGoogleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
@Override
public void onCameraMove() {
invalidate();
}
});
if (mMapReadyCallback != null) {
mMapReadyCallback.onMapReady(googleMap);
}
}
public void setPoints(LatLng pointA, LatLng pointB) {
mPointA = pointA;
mPointB = pointB;
}
}
MainActivity
赞:
public class MainActivity extends AppCompatActivity {
private static final String MAP_VIEW_BUNDLE_KEY = "MapViewBundleKey";
static final LatLng MAIDAN = new LatLng(50.450891, 30.522843);
static final LatLng SOPHIA = new LatLng(50.452967, 30.514498);
static final LatLng INITIAL_MAP_CENTER = new LatLng(50.452011, 30.518766);
static final int INITIAL_ZOOM = 15;
private GoogleMap mGoogleMap;
private EnhancedMapView mMapView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Bundle mapViewBundle = null;
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle(MAP_VIEW_BUNDLE_KEY);
}
mMapView = (EnhancedMapView) findViewById(R.id.mapview);
mMapView.onCreate(mapViewBundle);
mMapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
mMapView.setPoints(MAIDAN, SOPHIA);
mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(INITIAL_MAP_CENTER, INITIAL_ZOOM));
}
});
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Bundle mapViewBundle = outState.getBundle(MAP_VIEW_BUNDLE_KEY);
if (mapViewBundle == null) {
mapViewBundle = new Bundle();
outState.putBundle(MAP_VIEW_BUNDLE_KEY, mapViewBundle);
}
mMapView.onSaveInstanceState(mapViewBundle);
}
@Override
protected void onResume() {
super.onResume();
mMapView.onResume();
}
@Override
protected void onStart() {
super.onStart();
mMapView.onStart();
}
@Override
protected void onStop() {
super.onStop();
mMapView.onStop();
}
@Override
protected void onPause() {
mMapView.onPause();
super.onPause();
}
@Override
protected void onDestroy() {
mMapView.onDestroy();
super.onDestroy();
}
@Override
public void onLowMemory() {
super.onLowMemory();
mMapView.onLowMemory();
}
}
和activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.test.just.googlemapsgeneral.views.EnhancedMapView
android:id="@+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</RelativeLayout>
你有这样的东西:
注意!您应该使用 Path
而不是画线。这只是方法,不是完整的解决方案。
我想制作一个沿路径移动标记的动画。
所以我考虑了如何使用比路径线稍暗的标记来做到这一点。并非常缓慢地移除和更新其位置,以便它看起来像是在移动。但我只是不认为那是视频中发生的事情。因为市场完全呈现出路径的形状。它完美地流动。
这是我目前的情况:
fun showLineAtUsersLocation(loc_destination: LatLng) {
val currentLoc = activity.getCachedCurrentLoc()
val pattern = Arrays.asList(Dash(), Gap(convertDpToPixel(6).toFloat()))
val polyLineOptions: PolylineOptions = PolylineOptions()
.add(currentLoc)
.add(loc_destination)
.geodesic(true)
.pattern(pattern)
.width(convertDpToPixel(8).toFloat())
googleMap.addPolyline(polyLineOptions)
}
但这只是显示了一条从一点到另一点的叠加线。没有真正遵循路径。我应该寻找什么?
我看到 ios 有一个 class good for this GMSStyleSpan 来处理图像。但我在 android 中找不到等效项。我没有看到跨度 class 或样式 class 我可以参考这方面的任何想法吗?或者甚至在标记上使用 AnomatedVectorDrawable
对于此类(实际上是任何类型的)动画,您可以使用 View Canvas 动画。此方法需要 MapView
-based custom view,实现:
在 MapView 上绘图 canvas;
自定义线条样式(圆而不是简单的线);
绑定到 Lat/Lon 地图坐标的路径
执行动画。
在 MapView 上绘制需要覆盖 dispatchDraw()
。自定义线条样式需要 setPathEffect()
method of Paint
class 允许为 "circle stamp"(以像素为单位)创建创建路径,这将重复每个 "advance"(也以像素为单位),类似这样:
mCircleStampPath = new Path();
mCircleStampPath.addCircle(0,0, CIRCLE_RADIUS, Path.Direction.CCW);
mCircleStampPath.close();
要将屏幕上的路径绑定到 Lat/Lon 坐标,可以使用 Projection.toScreenLocation()
needed, that requires GoogleMap
object so custom view should implements OnMapReadyCallback
for receive it. For continuous animation postInvalidateDelayed()
。因此,使用基于 MapView
的自定义 EnhancedMapView
:
public class EnhancedMapView extends MapView implements OnMapReadyCallback {
private static final float CIRCLE_RADIUS = 10;
private static final float CIRCLE_ADVANCE = 3.5f * CIRCLE_RADIUS; // spacing between each circle stamp
private static final int FRAMES_PER_SECOND = 30;
private OnMapReadyCallback mMapReadyCallback;
private GoogleMap mGoogleMap;
private LatLng mPointA;
private LatLng mPointB;
private float mCirclePhase = 0; // amount to offset before the first circle is stamped
private Path mCircleStampPath;
private Paint mPaintLine;
private final Path mPathFromAtoB = new Path();
public EnhancedMapView(@NonNull Context context) {
super(context);
init();
}
public EnhancedMapView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public EnhancedMapView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public EnhancedMapView(@NonNull Context context, @Nullable GoogleMapOptions options) {
super(context, options);
init();
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
canvas.save();
drawLineFomAtoB(canvas);
canvas.restore();
// perform continuous animation
postInvalidateDelayed(1000 / FRAMES_PER_SECOND);
}
private void drawLineFomAtoB(Canvas canvas) {
if (mGoogleMap == null || mPointA == null || mPointB == null) {
return;
}
final Projection mapProjection = mGoogleMap.getProjection();
final Point pointA = mapProjection.toScreenLocation(mPointA);
final Point pointB = mapProjection.toScreenLocation(mPointB);
mPathFromAtoB.rewind();
mPathFromAtoB.moveTo(pointB.x, pointB.y);
mPathFromAtoB.lineTo(pointA.x, pointA.y);
// change phase for circles shift
mCirclePhase = (mCirclePhase < CIRCLE_ADVANCE)
? mCirclePhase + 1.0f
: 0;
mPaintLine.setPathEffect(new PathDashPathEffect(mCircleStampPath, CIRCLE_ADVANCE, mCirclePhase, PathDashPathEffect.Style.ROTATE));
canvas.drawPath(mPathFromAtoB, mPaintLine);
}
private void init() {
setWillNotDraw(false);
mCircleStampPath = new Path();
mCircleStampPath.addCircle(0,0, CIRCLE_RADIUS, Path.Direction.CCW);
mCircleStampPath.close();
mPaintLine = new Paint();
mPaintLine.setColor(Color.BLACK);
mPaintLine.setStrokeWidth(1);
mPaintLine.setStyle(Paint.Style.STROKE);
mPaintLine.setPathEffect(new PathDashPathEffect(mCircleStampPath, CIRCLE_ADVANCE, mCirclePhase, PathDashPathEffect.Style.ROTATE));
postInvalidate();
}
@Override
public void getMapAsync(OnMapReadyCallback callback) {
mMapReadyCallback = callback;
super.getMapAsync(this);
}
@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
mGoogleMap.setOnCameraMoveListener(new GoogleMap.OnCameraMoveListener() {
@Override
public void onCameraMove() {
invalidate();
}
});
if (mMapReadyCallback != null) {
mMapReadyCallback.onMapReady(googleMap);
}
}
public void setPoints(LatLng pointA, LatLng pointB) {
mPointA = pointA;
mPointB = pointB;
}
}
MainActivity
赞:
public class MainActivity extends AppCompatActivity {
private static final String MAP_VIEW_BUNDLE_KEY = "MapViewBundleKey";
static final LatLng MAIDAN = new LatLng(50.450891, 30.522843);
static final LatLng SOPHIA = new LatLng(50.452967, 30.514498);
static final LatLng INITIAL_MAP_CENTER = new LatLng(50.452011, 30.518766);
static final int INITIAL_ZOOM = 15;
private GoogleMap mGoogleMap;
private EnhancedMapView mMapView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Bundle mapViewBundle = null;
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle(MAP_VIEW_BUNDLE_KEY);
}
mMapView = (EnhancedMapView) findViewById(R.id.mapview);
mMapView.onCreate(mapViewBundle);
mMapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(GoogleMap googleMap) {
mGoogleMap = googleMap;
mMapView.setPoints(MAIDAN, SOPHIA);
mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(INITIAL_MAP_CENTER, INITIAL_ZOOM));
}
});
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Bundle mapViewBundle = outState.getBundle(MAP_VIEW_BUNDLE_KEY);
if (mapViewBundle == null) {
mapViewBundle = new Bundle();
outState.putBundle(MAP_VIEW_BUNDLE_KEY, mapViewBundle);
}
mMapView.onSaveInstanceState(mapViewBundle);
}
@Override
protected void onResume() {
super.onResume();
mMapView.onResume();
}
@Override
protected void onStart() {
super.onStart();
mMapView.onStart();
}
@Override
protected void onStop() {
super.onStop();
mMapView.onStop();
}
@Override
protected void onPause() {
mMapView.onPause();
super.onPause();
}
@Override
protected void onDestroy() {
mMapView.onDestroy();
super.onDestroy();
}
@Override
public void onLowMemory() {
super.onLowMemory();
mMapView.onLowMemory();
}
}
和activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.test.just.googlemapsgeneral.views.EnhancedMapView
android:id="@+id/mapview"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
</RelativeLayout>
你有这样的东西:
注意!您应该使用 Path
而不是画线。这只是方法,不是完整的解决方案。