Home » Android » Android Google Maps – custom background behind all markers

Android Google Maps – custom background behind all markers

Posted by: admin May 14, 2020 Leave a comment

Questions:

I try to put a background between the map and my marker; this image should always (!) stay behind all markers.

I tried several way, e.g. showInfoWindow() on all my markers except the background image, but it seems as this would only help for the last one.

Then I tried a solution using GroundOverlay, but I need my image to always stay the same size (e.g. half the device size), independent of zoom factor.

Is there a solution on Android for this?

How to&Answers:

This is easy to do.

Add two markers instead of one. First add the background as a bitmap at lat_lng.

Something like this:

map.addMarker(new MarkerOptions()
      .position(lat_lng)
      .anchor(0.5f, 0.5f)
      .icon(BitmapDescriptorFactory.fromBitmap(bitmap)));

Then add your marker like you normally do at the same lat_lng.

Do the above for each of your markers.

Answer:

You can try this, calculate the dimensions according to zoom factor and apply those dimensions to the GroundOverlay.

float zoomLevel=mMap.getCameraPosition().zoom;
    //calculate meters*********************         
    myBounds = mMap.getProjection().getVisibleRegion().latLngBounds;
    myCenter=  mMap.getCameraPosition().target;
    if (myCenter.latitude==0 || myCenter.longitude==0) {
        myCenter=new LatLng(myLocation.getLatitude(),myLocation.getLongitude());
    }

    LatLng ne = myBounds.northeast;

    // r = radius of the earth in statute miles
    double r = 3963.0;  

    // Convert lat or lng from decimal degrees into radians (divide by 57.2958)
    double lat1 = myCenter.latitude / 57.2958; 
    double lon1 = myCenter.longitude / 57.2958;
    final double lat2 = ne.latitude / 57.2958;
    final double lon2 = ne.longitude / 57.2958;

    // distance = circle radius from center to Northeast corner of bounds
    double dis = r * Math.acos(Math.sin(lat1) * Math.sin(lat2) + 
      Math.cos(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1));

    //1 Meter = 0.000621371192237334 Miles
    double meters_calc=dis/0.000621371192237334;

    float factor=1;  
    if (zoomLevel==15) {  // my default zoom level yours can be different
        metersoverlay=meters_calc;  // global variable metersoverlay set
    }
    else { // if my zoom level change then I have to calculate dimension scale factor
        factor=(float) (meters_calc/metersoverlay); 
    }

    //******************************* now we are ready to set dimension of background overlay
    float dimensions=1000*factor; 
    loadingGroundOverlayBg.setDimensions(dimensions);

Answer:

Your best option is to use a GroundOverlay to show your background, adding and removing it when the view changes, and computing it’s dimensions based on the VisibleRegion of the map so it maintains the correct size between zooms.

From the documentation:

Position

There are two ways to specify the position of the ground overlay:

  • Using a location: You must provide an image of the ground overlay, a LatLng to which the anchor will be fixed and the width of the overlay (in meters). The anchor is, by default, 50% from the top of the image and 50% from the left of the image. This can be changed. You can optionally provide the height of the overlay (in meters). If you do not provide the height of the overlay, it will be automatically calculated to preserve the proportions of the image.

  • Using a Bounds: You must provide a LatLngBounds which will contain the image.

The following example uses a OnCameraChangeListener to remove and add a GroundOverlay that shows the ic_launcher drawable in the center of the map, 75% of the width of the map (Removing and adding the GroundOverlay on the onCameraChange event ensures that the size of your image is always the same independently of the zoom level).

public class MyMapActivity extends Activity implements OnCameraChangeListener {
    GroundOverlay backgroundOverlay = null;
    GoogleMap map;

    // ...

    @Override
    public void onCameraChange(final CameraPosition cameraPosition) {
        if (backgroundOverlay != null) {
            backgroundOverlay.remove();
        }

        VisibleRegion visibleRegion = map.getProjection()
                .getVisibleRegion();

        Location nearLeftLocation = new Location("nearLeftLocation");
        nearLeftLocation.setLatitude(visibleRegion.nearLeft.latitude);
        nearLeftLocation.setLongitude(visibleRegion.nearLeft.longitude);

        Location nearRightLocation = new Location("nearRightLocation");
        nearRightLocation.setLatitude(visibleRegion.nearRight.latitude);
        nearRightLocation.setLongitude(visibleRegion.nearRight.longitude);

        float width = nearLeftLocation.distanceTo(nearRightLocation);

        GroundOverlayOptions background = new GroundOverlayOptions()
                .image(BitmapDescriptorFactory.fromResource(R.drawable.ic_launcher))
                .position(cameraPosition.target, width * 0.75f);

        backgroundOverlay = map.addGroundOverlay(background);
    }
}

UPDATE:

As I say in the comments we can setPosition and setDimensions on the existing GroundOverlay instead of adding/removing it on every camera change.

I have tested it on several devices and it seems to reduce the flickering on low end devices, but still not a perfect solution (image resizes between zooms). I am pretty sure that this is the best we can get using GroundOverlays.

public class MyMapActivity extends Activity implements OnCameraChangeListener {
    GroundOverlay backgroundOverlay = null;
    GoogleMap map;

    // ...

    @Override
    public void onCameraChange(final CameraPosition cameraPosition) {
        VisibleRegion visibleRegion = map.getProjection().getVisibleRegion();

        Location nearLeftLocation = new Location("nearLeftLocation");
        nearLeftLocation.setLatitude(visibleRegion.nearLeft.latitude);
        nearLeftLocation.setLongitude(visibleRegion.nearLeft.longitude);

        Location nearRightLocation = new Location("nearRightLocation");
        nearRightLocation.setLatitude(visibleRegion.nearRight.latitude);
        nearRightLocation.setLongitude(visibleRegion.nearRight.longitude);

        float width = nearLeftLocation.distanceTo(nearRightLocation);

        if (backgroundOverlay == null) {
            GroundOverlayOptions background = new GroundOverlayOptions()
                    .image(BitmapDescriptorFactory.fromResource(R.drawable.ic_launcher))
                    .position(cameraPosition.target, width * 0.75f);

            backgroundOverlay = map.addGroundOverlay(background);
        } else {
            backgroundOverlay.setPosition(cameraPosition.target);
            backgroundOverlay.setDimensions(width * 0.75f);
        }
    }
}

UPDATE

I have found another solution. If your background is a plain, semitransparent color, you can use a TileOverlay to set it below all markers.

The idea is to create a TileProvider that returns always a very small (2×2 pixels in my example) image that will be draw below all your markers. To ensure performance, the Tile returned by the TileProvider will always be the same.

Here is the code:

public class BackgroundTileProvider implements TileProvider {
    private Tile tile;

    @Override
    public Tile getTile(int x, int y, int zoom) {
        if (tile == null) {
            // Create a very small (for performance) bitmap with alpha
            Bitmap bmp = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888);

            // Draw the desired color to use as background
            Canvas canvas = new Canvas(bmp);
            canvas.drawColor(Color.argb(150, 255, 0, 0));

            // Get the bytes and create the Tile
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);

            tile = new Tile(2, 2, stream.toByteArray());
        }

        return tile;
    }
}

You will need to add the TileOverlay to your map as follows:

TileProvider tileProvider = new BackgroundTileProvider();
TileOverlay tileOverlay = map.addTileOverlay(
    new TileOverlayOptions().tileProvider(tileProvider));

The result will look like this:

enter image description here