Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google Maps - making an animation like grab app to show the path direction

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

like image 611
j2emanue Avatar asked Oct 16 '22 09:10

j2emanue


1 Answers

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:

animated line

NB! You should use Path instead of drawing lines. And that is just approach, not complete solution.

like image 165
Andrii Omelchenko Avatar answered Oct 21 '22 04:10

Andrii Omelchenko