I would like to make an animation which does a moving marker along a path.
so i thought about how to do this using a marker that is slightly darker than the path line. and removing and updating its position very slowly so that it looks like its moving. but i just don't think that's what's happening in the video. because the market takes on the shape of the path entirely. it flows perfectly.
here is what i have so far:
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)
}
but this is just showing an overlay line from one point to the other. not really following a path. What should i be looking for instead?
I saw that ios has a has a class good for this GMSStyleSpan to manipuate the image. but i cannot find the equivalent in android. i dont see a span class or style class i can get a reference to any ideas on this ? or even with AnomatedVectorDrawable on the marker instead
For such (and actually any kind of) animation you can use View Canvas animation. This approach requires MapView
-based custom view, that implements:
drawing over the MapView canvas;
customizing line styles (circles instead of a simple line);
binding path to Lat/Lon coordinates of map
performing animation.
Drawing over the MapView needs to override dispatchDraw()
. Customizing line styles needs setPathEffect()
method of Paint
class that allows to create create path for "circle stamp" (in pixels), which will repeated every "advance" (in pixels too), something like that:
mCircleStampPath = new Path();
mCircleStampPath.addCircle(0,0, CIRCLE_RADIUS, Path.Direction.CCW);
mCircleStampPath.close();
For binding path on screen to Lat/Lon coordinates Projection.toScreenLocation()
needed, that requires GoogleMap
object so custom view should implements OnMapReadyCallback
for receive it. For continuous animation postInvalidateDelayed()
can be used. So, with full source code of MapView
-based custom 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
like:
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();
}
}
and 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>
you got something like:
NB! You should use Path
instead of drawing lines. And that is just approach, not complete solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With