Thursday 30 September 2021

Google Maps SDK for Android: Smoothly animating the camera to a new location, rendering all the tiles along the way

Background

Many similar questions seem to have been asked on SO before (most notably android google maps not loading the map when using GoogleMap.AnimateCamera() and How can I smoothly pan a GoogleMap in Android?), but none of the answers or comments posted throughout those threads have given me a firm idea of how to do this.

I initially thought that it would be as simple as just calling animateCamera(CameraUpdateFactory.newLatLng(), duration, callback) but like the OP of the first link above, all I get is a gray or very blurry map until the animation completes, even if I slow it down to tens of seconds long!

I've managed to find and implement this helper class that does a nice job of allowing the tiles to render along the way, but even with a delay of 0, there is a noticeable lag between each animation.

Code

OK, time for some code. Here's the (slightly-modified) helper class:

package com.coopmeisterfresh.googlemaps.NativeModules;

import android.os.Handler;

import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.GoogleMap;

import java.util.ArrayList;
import java.util.List;

public class CameraUpdateAnimator implements GoogleMap.OnCameraIdleListener {
    private final GoogleMap mMap;
    private final GoogleMap.OnCameraIdleListener mOnCameraIdleListener;

    private final List<Animation> cameraUpdates = new ArrayList<>();

    public CameraUpdateAnimator(GoogleMap map, GoogleMap.
        OnCameraIdleListener onCameraIdleListener) {
        mMap = map;
        mOnCameraIdleListener = onCameraIdleListener;
    }

    public void add(CameraUpdate cameraUpdate, boolean animate, long delay) {
        if (cameraUpdate != null) {
            cameraUpdates.add(new Animation(cameraUpdate, animate, delay));
        }
    }

    public void clear() {
        cameraUpdates.clear();
    }

    public void execute() {
        mMap.setOnCameraIdleListener(this);
        executeNext();
    }

    private void executeNext() {
        if (cameraUpdates.isEmpty()) {
            mOnCameraIdleListener.onCameraIdle();
        } else {
            final Animation animation = cameraUpdates.remove(0);

            new Handler().postDelayed(() -> {
                if (animation.mAnimate) {
                    mMap.animateCamera(animation.mCameraUpdate);
                } else {
                    mMap.moveCamera(animation.mCameraUpdate);
                }
            }, animation.mDelay);
        }
    }

    @Override
    public void onCameraIdle() {
        executeNext();
    }

    private static class Animation {
        private final CameraUpdate mCameraUpdate;
        private final boolean mAnimate;
        private final long mDelay;

        public Animation(CameraUpdate cameraUpdate, boolean animate, long delay) {
            mCameraUpdate = cameraUpdate;
            mAnimate = animate;
            mDelay = delay;
        }
    }
}

And my code to implement it:

// This is actually a React Native Component class, but I doubt that should matter...?
public class NativeGoogleMap extends SimpleViewManager<MapView> implements
    OnMapReadyCallback, OnRequestPermissionsResultCallback {

    // ...Other unrelated methods removed for brevity

    private void animateCameraToPosition(LatLng targetLatLng, float targetZoom) {
        // googleMap is my GoogleMap instance variable; it
        // gets properly initialised in another class method
        CameraPosition currPosition = googleMap.getCameraPosition();
        LatLng currLatLng = currPosition.target;
        float currZoom = currPosition.zoom;

        double latDelta = targetLatLng.latitude - currLatLng.latitude;
        double lngDelta = targetLatLng.longitude - currLatLng.longitude;

        double latInc = latDelta / 5;
        double lngInc = lngDelta / 5;

        float zoomInc = 0;
        float minZoom = googleMap.getMinZoomLevel();
        float maxZoom = googleMap.getMaxZoomLevel();

        if (lngInc > 15 && currZoom > minZoom) {
            zoomInc = (minZoom - currZoom) / 5;
        }

        CameraUpdateAnimator animator = new CameraUpdateAnimator(googleMap,
            () -> googleMap.animateCamera(CameraUpdateFactory.zoomTo(
            targetZoom), 5000, null));

        for (double nextLat = currLatLng.latitude, nextLng = currLatLng.
            longitude, nextZoom = currZoom; Math.abs(nextLng) < Math.abs(
            targetLatLng.longitude);) {
            nextLat += latInc;
            nextLng += lngInc;
            nextZoom += zoomInc;

            animator.add(CameraUpdateFactory.newLatLngZoom(new
                LatLng(nextLat, nextLng), (float)nextZoom), true);
        }

        animator.execute();
    }
}

Question

Is there a better way to accomplish this seemingly-simple task? I'm thinking that perhaps I need to move my animations to a worker thread or something; would that help?

Thanks for reading (I know it was an effort :P)!

Update 30/09/2021

I've updated the code above in line with Andy's suggestions in the comments and although it works (albeit with the same lag and rendering issues), the final algorithm will need to be a bit more complex since I want to zoom out to the longitudinal delta's half-way point, then back in as the journey continues.

Doing all these calculations at once, as well as smoothly rendering all the necessary tiles simultaneously, seems to be way too much for the cheap mobile phone that I'm testing on. Or is this a limitation of the API itself? In any case, how can I get all of this working smoothly, without any lag whatsoever between queued animations?



from Google Maps SDK for Android: Smoothly animating the camera to a new location, rendering all the tiles along the way

No comments:

Post a Comment