1

I am searching for a way to animate the dots between two markers on a google map in android device.
So what i want in the end is the following line between the two images:

enter image description here

and it would be used like this typical google polyline implementation:

enter image description here

lets say there is a point A and a Point B. if im directing the user to point B, then the line animates to from point A to point B so the user knows to walk in this direction.

to achieve this i thought i could get the points out of the polyLine and remove them and add them back rapidily. so lets say i had 5 points in the polyLine, i would remove position 1 , then put it back, then remove position 2, and put it back, to simulate this animation.

but it does not work . once hte polyline is set it seems i cannot alter it. you have any suggestions ?

val dotPattern = Arrays.asList(Dot(), Gap(convertDpToPixel(7).toFloat()))
            val polyLineOptions: PolylineOptions = PolylineOptions()
                    .add(usersLocation)
                    .add(users_destination)
                    .pattern(dotPattern)
                    .width(convertDpToPixel(6).toFloat())
            dottedPolyLine = googleMap.addPolyline(polyLineOptions)

dottedPolyLine?.points?.removeAt(1) // here as a test if my idea i try removing a point but it looks like a point here means current location or destination so there always 2. i thought a point would be one of the dots.

j2emanue
  • 60,549
  • 65
  • 286
  • 456

1 Answers1

1

You can use MapView-based custom view View Canvas animationlike in this answer:

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.

but not draw path directly from point A to point B, but from point A to point C that animated from A to B. To get current position of point C you can use SphericalUtil.interpolate() from Google Maps Android API Utility Library. Something like that:

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 static final int ANIMATION_DURATION = 10000;


    private OnMapReadyCallback mMapReadyCallback;
    private GoogleMap mGoogleMap;
    private LatLng mPointA;
    private LatLng mPointB;
    private LatLng mPointC;

    private float mCirclePhase = 0;                                     // amount to offset before the first circle is stamped
    private Path mCircleStampPath;
    private Paint mPaintLine;
    private final Path mPathFromAtoC = new Path();
    private long mStartTime;
    private long mElapsedTime;

    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 one shot animation
        mElapsedTime = System.currentTimeMillis() - mStartTime;
        if (mElapsedTime < ANIMATION_DURATION) {
            postInvalidateDelayed(1000 / FRAMES_PER_SECOND);
        }
    }

    private void drawLineFomAtoB(Canvas canvas) {
        if (mGoogleMap == null || mPointA == null || mPointB == null) {
            return;
        }

        // interpolate current position
        mPointC = SphericalUtil.interpolate(mPointA, mPointB, (float) mElapsedTime / (float)ANIMATION_DURATION);

        final Projection mapProjection = mGoogleMap.getProjection();
        final Point pointA = mapProjection.toScreenLocation(mPointA);
        final Point pointC = mapProjection.toScreenLocation(mPointC);

        mPathFromAtoC.rewind();
        mPathFromAtoC.moveTo(pointC.x, pointC.y);
        mPathFromAtoC.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(mPathFromAtoC, 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));

        // start animation
        mStartTime = System.currentTimeMillis();    
        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;
    }

}

NB! This is just idea, not full tested code.

Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79
  • your work is genius. appreciate your help. one thing i need. i dont want the line to touch the markers so much. as as soon as the line reaches the markers it should not show anymore. how can i control that ? i wish there was like a z-index to make the markers appear over the line. what do you suggest ? i just need the line to not cross ontop of the markers both. – j2emanue Dec 31 '18 at 11:11
  • @j2emanue Thanx, but I'm just Android engineer :) Because of drawing directly on View canvas there is no possibilities to take into account Google Maps z-index. Possible workaround - convert markers LatLng into screen coords (via `mapProjection.toScreenLocation(marker.getPosition());`) and draw markers manually on canvas after `drawLineFomAtoB(canvas);` call. Marker can be drawn as bitmap like in answers for [this](https://stackoverflow.com/q/13361231/6950238) question. Or you can draw line slightly afrer point A and slightly before point B to avoid intersection with marker. – Andrii Omelchenko Dec 31 '18 at 11:23