4

I'm trying to get the snapshot of a GoogleMap (from MapView) on background, return it with a callback and show it in an ImageView. While doing this, I'm trying to make getting snapshot part independent from the layout as much as possible (so, trying to avoid adding a MapView to the layout). What I've tried was creating a MapView programmatically with given size, adding some paths and pins on it, getting the snapshot and returning it (all subscribed using RxJava) but I'm getting the following error and app is closing without even a "app stopped working" dialog.

FATAL EXCEPTION: androidmapsapi-Snapshot Process:
com.package.name.debug, PID: 2159
    java.lang.IllegalArgumentException: width and height must be > 0
    at android.graphics.Bitmap.createBitmap(Bitmap.java:1013)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:980)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:930)
    at android.graphics.Bitmap.createBitmap(Bitmap.java:891)
    at com.google.maps.api.android.lib6.impl.ec.b(:com.google.android.gms.DynamiteModulesB@11975940:4)
    at com.google.maps.api.android.lib6.impl.bf.run(:com.google.android.gms.DynamiteModulesB@11975940:4)
    at java.lang.Thread.run(Thread.java:764)

Here is the code for getting snapshot

fun bitmapFor(mapData: MapData, specs: Specs): Single<Bitmap?> {
    val mapView = MapView(context)
    mapView.layoutParams = ViewGroup.LayoutParams(specs.mapWidth, specs.mapHeight)

    return mapView.getMap()
            .subscribeOn(AndroidSchedulers.mainThread())
            .map { googleMap ->
                setupMap(googleMap, mapData) // Adds paths and pins
                googleMap
            }
            .flatMap { googleMap ->
                googleMap.snapshot(mapData)
            }
}

private fun MapView.getMap(): Single<GoogleMap> {
    return Single.create { subscriber ->

        val handler = Handler(Looper.getMainLooper())
        handler.post {
            onCreate(null)
            onStart()
            onResume()

            getMapAsync { googleMap ->
                subscriber.onSuccess(googleMap)
            }
        }
    }
}

private fun GoogleMap.snapshot(mapData: MapData): Single<Bitmap> {
    return Single.create { subscriber ->
        snapshot { bitmap ->
            saveMap(mapData.hashCode().toString(), bitmap)
            subscriber.onSuccess(bitmap)
        }
    }
}

And in the UI class, I'm doing

viewModel.mapProvider
        .bitmapFor(map, viewModel.mapData, Specs(map.measuredWidth, map.measuredHeight))
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(this::onMap, this::onError) // onMap set's the image bitmap
iamkaan
  • 1,495
  • 2
  • 23
  • 43
  • 1
    How about to use [Google Maps Static API](https://developers.google.com/maps/documentation/static-maps/?hl=en), like in [this](https://stackoverflow.com/questions/5324004/how-to-display-static-google-map-on-android-imageview) question? – Andrii Omelchenko Feb 05 '18 at 12:08
  • Your `width` and `height` is getting zero while taking snapshot. That means you have to check first any layout bound is visible on screen or not before taking snapshot. – Aditya Feb 05 '18 at 12:15
  • @AndriiOmelchenko That's what I was using so far, but it has some limitations unfortunately. It's limiting the number of characters in the URL, which forces me to reduce precision of the path. @Heisen-Berg That's what I thought, and I tried some stuff to prevent that (incl. using `mapView.post` and hardcoding the size) but none worked.. – iamkaan Feb 05 '18 at 12:18
  • Did you use [encoded pollines](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) in your URL? – Andrii Omelchenko Feb 05 '18 at 12:22
  • Didn't know this one but another issue we have with Static Maps is that we'd like to avoid posting the coordinates in a URL as the data is sensitive. Thanks a lot for the suggestion though! – iamkaan Feb 05 '18 at 12:29
  • Also you can download map image and draw whatever you need locally on downloaded bitmap: you know map bounds and can create custom `latLngToImageCoords()` and `imageCoordsToLatLng()`. – Andrii Omelchenko Feb 05 '18 at 12:33
  • Don't I need to have a GoogleMaps/MapView for that? Any documentations? I couldn't find anything on Google. – iamkaan Feb 05 '18 at 12:50
  • Please take a look at updated answer. – Andrii Omelchenko Feb 08 '18 at 18:01

1 Answers1

3

Please see update!

Seems it impossible to get Google Map snapshot on background on invisible MapView because map didn't even starting to load, when it is invisible. You can set correct size for invisible MapVew (e.g. based on this article), but seems it will be empty black bar with "Google" logo. Possible simplest workaround - still show MapView, but disable controls and gestures on it:

`public void onMapReady(GoogleMap googleMap) {
    mGoogleMap = googleMap;
    mGoogleMap.getUiSettings().setAllGesturesEnabled(false);
    ...
}`

or download map image via Static Map API and draw whatever you need locally on downloaded bitmap.

UPDATE:

It's possible to get Google Map snapshot on background on invisible MapView via setting it to Lite Mode on creation:

...
GoogleMapOptions options = new GoogleMapOptions()
        .compassEnabled(false)
        .mapToolbarEnabled(false)
        .camera(CameraPosition.fromLatLngZoom(KYIV,15))
        .liteMode(true);
mMapView = new MapView(this, options);
...

And it's necessary to determine the MapView size in onMapReady() via measure() and layout() calls to initiate map image loading - than, after map image loaded, it's possible get it in onMapLoaded() callback:

mMapView.setDrawingCacheEnabled(true);
mMapView.measure(View.MeasureSpec.makeMeasureSpec(mMapWidth, View.MeasureSpec.EXACTLY),
        View.MeasureSpec.makeMeasureSpec(mMapHeight, View.MeasureSpec.EXACTLY));
mMapView.layout(0, 0, mMapWidth, mMapHeight);
mMapView.buildDrawingCache(true);
Bitmap b = Bitmap.createBitmap(mMapView.getDrawingCache());  // <- that is bitmap with map image
mMapView.setDrawingCacheEnabled(false);

It is mandatory to call onCreate() on MapView object before getMapAsyhc(), otherwise no map will appear.

Full source code:

public class MainActivity extends AppCompatActivity {

    private static final String MAP_VIEW_BUNDLE_KEY = "MapViewBundleKey";
    static final LatLng KYIV = new LatLng(50.450311, 30.523730);

    private ImageView mImageView;

    private MapView mMapView;
    // dimensions of map image
    private int mMapWidth = 600;
    private int mMapHeight = 800;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mImageView = (ImageView) findViewById(R.id.image_view);

        Bundle mapViewBundle = null;
        if (savedInstanceState != null) {
            mapViewBundle = savedInstanceState.getBundle(MAP_VIEW_BUNDLE_KEY);
        }

        GoogleMapOptions options = new GoogleMapOptions()
                .compassEnabled(false)
                .mapToolbarEnabled(false)
                .camera(CameraPosition.fromLatLngZoom(KYIV,15))
                .liteMode(true);
        mMapView = new MapView(this, options);
        mMapView.onCreate(mapViewBundle);

        mMapView.getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(GoogleMap googleMap) {

                // form your card (add markers, path etc.) here: 
                googleMap.addMarker(new MarkerOptions()
                        .position(KYIV)
                        .title("Kyiv"));

                // set map size in pixels and initiate image loading
                mMapView.measure(View.MeasureSpec.makeMeasureSpec(mMapWidth, View.MeasureSpec.EXACTLY),
                        View.MeasureSpec.makeMeasureSpec(mMapHeight, View.MeasureSpec.EXACTLY));
                mMapView.layout(0, 0, mMapWidth, mMapHeight);

                googleMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
                    @Override
                    public void onMapLoaded() {
                        mMapView.setDrawingCacheEnabled(true);
                        mMapView.measure(View.MeasureSpec.makeMeasureSpec(mMapWidth, View.MeasureSpec.EXACTLY),
                                View.MeasureSpec.makeMeasureSpec(mMapHeight, View.MeasureSpec.EXACTLY));
                        mMapView.layout(0, 0, mMapWidth, mMapHeight);
                        mMapView.buildDrawingCache(true);
                        Bitmap b = Bitmap.createBitmap(mMapView.getDrawingCache());
                        mMapView.setDrawingCacheEnabled(false);
                        mImageView.setImageBitmap(b);

                    }
                });

            }
        });
    }

    @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);
        }
    }
}

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="{PACKAGE_NAME}.MainActivity">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        />

</RelativeLayout>

which get screenshot with marker on map:

MapView screenshot example

If you wish to show the My Location dot on your lite mode map and use the default location source, you will need to call onResume() and onPause():

...
@Override
protected void onResume() {
    super.onResume();
    if (mMapView != null) {
        mMapView.onResume();
    }
}

@Override
protected void onPause() {
    if (mMapView != null) {
        mMapView.onPause();
    }
    super.onPause();
}
...

because the location source will only update between these calls.

Andrii Omelchenko
  • 13,183
  • 12
  • 43
  • 79