Changing TileOverlay opacity for Android's Google Maps
You've got your app setup and integrated so that it can display a Google Maps MapView or SupportMapFragment (or whatever other map component that you've chosen) using the Google Maps Android API v2 via Google Play services. Great.
Now you want to display TileOverlays on top of your map. Maybe you want to tie into OpenWeatherMap's map layers service to display the current radar precipitation images on your map. So you configure your TileOverlay with an UrlTileProvider and point it at OWM's tile server endpoint, supplying the x, y, and zoom parameters. It all works great.
But the tiles show up as opaque. They block out everything behind them. The big storm rolling into town is obscuring the town's name. So let's just change the opacity from 100% to something like 75%. Easy to do using the Google Maps JavaScript API; should be easy to do in the Android API.
Except changing the opacity of TileOverlays isn't currently supported. You've got two options; you can setup your own tile server and change the opacity there, then simply point your UrlTileProvider to your server rather than OWM's, or you can do it on the Android device itself. I chose to do it on the device because my app receives hundreds of thousands of hits already, and images are big bandwidth, and honestly having to do image manipulation and caching and all that gave me a headache just thinking about it.
So the first thing we have to do is write a custom TileProvider implementation; unfortunately, the UrlTileProvider.getTile() method is marked final. (Not sure why; dumb design call if you ask me.) So that means our custom TileProvider has to not only change the opacity of the tile image, but it first has to fetch it too. Because some Google engineer didn't have the foresight to not make the UrlTileProvider's getTile() method not final and therefore hindering any reuse of the tried, true, and tested code of that class. Thanks Google. (Technically, you could wrap a UrlTileProvider in our custom implementation's getTile() method to do the dirty work of fetching the tile image; it worked, I tried that initially, but decided to just do the heavy lifting myself for performance reasons.)
Anyway, the following is a simple TileProvider that we can plugin to accomplish all of this:
/** * A custom {@link TileProvider} implementation that does what the {@link UrlTileProvider} does except it lets us * change the opacity of the tile images that are retrieved. * * In an ideal world, the {@link UrlTileProvider#getTile(int,int,int) UrlTileProvider.getTile()} method wouldn't be * marked as <code>final</code>, and we could've simply extended that class instead of rewriting the wheel. Until * Google fixes that little mistake, we're stuck with fetching the tile images as well as changing their opacity. * * <p>This code is public domain; use freely, but at your own risk.</p> * * @author Jonathon Freeman <[email protected]> * */ public class TranslucentUrlTileProvider implements TileProvider { private String url; private Paint opacityPaint = new Paint(); /** * This constructor assumes the <code>url</code> parameter contains three placeholders for the x- and y-positions of * the tile as well as the zoom level of the tile. The placeholders are assumed to be <code>{x}</code>, * <code>{y}</code>, and <code>{zoom}</code>. An example * for an OpenWeatherMap URL would be: http://tile.openweathermap.org/map/precipitation/{zoom}/{x}/{y}.png * * @param url The tile server's endpoint URL, from which to retrieve {@link Tile} images */ public TranslucentUrlTileProvider(String url) { this.url = url; setOpacity(75); } /** * Sets the desired opacity of map {@link Tile}s, as a percentage where 0% is invisible and 100% is completely opaque. * @param opacity The desired opacity of map {@link Tile}s (as a percentage between 0 and 100, inclusive) */ public void setOpacity(int opacity) { int alpha = (int)Math.round(opacity * 2.55); // 2.55 = 255 * 0.01 opacityPaint.setAlpha(alpha); } @Override public Tile getTile(int x, int y, int zoom) { URL tileUrl = getTileUrl(x, y, zoom); Tile tile = null; ByteArrayOutputStream stream = null; try { Bitmap image = BitmapFactory.decodeStream(tileUrl.openConnection().getInputStream()); image = adjustOpacity(image); stream = new ByteArrayOutputStream(); image.compress(Bitmap.CompressFormat.PNG, 100, stream); byte[] byteArray = stream.toByteArray(); tile = new Tile(256, 256, byteArray); } catch(IOException e) { e.printStackTrace(); } finally { if(stream != null) { try { stream.close(); } catch(IOException e) {} } } return tile; } /** * Helper method that returns the {@link URL} of the tile image for the given x/y/zoom location. * * <p>This method assumes the URL string provided in the constructor contains three placeholders for the x- * and y-positions as well as the zoom level of the desired tile; <code>{x}</code>, <code>{y}</code>, and * <code>{zoom}</code>. An example for an OpenWeatherMap URL would be: * http://tile.openweathermap.org/map/precipitation/{zoom}/{x}/{y}.png</p> * * @param x The x-position of the tile * @param y The y-position of the tile * @param zoom The zoom level of the tile * * @return The {@link URL} of the desired tile image */ private URL getTileUrl(int x, int y, int zoom) { String tileUrl = url .replace("{x}", Integer.toString(x)) .replace("{y}", Integer.toString(y)) .replace("{zoom}", Integer.toString(zoom)); try { return new URL(tileUrl); } catch(MalformedURLException e) { throw new AssertionError(e); } } /** * Helper method that adjusts the given {@link Bitmap}'s opacity to the opacity previously set via * {@link #setOpacity(int)}. Stolen from Elysium's comment at StackOverflow. * * @param bitmap The {@link Bitmap} whose opacity to adjust * @return A new {@link Bitmap} with an adjusted opacity * * @see http://stackoverflow.com/questions/14322236/making-tileoverlays-transparent#comment19934097_14329560 */ private Bitmap adjustOpacity(Bitmap bitmap) { Bitmap adjustedBitmap = Bitmap.createBitmap(256, 256, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(adjustedBitmap); canvas.drawBitmap(bitmap, 0, 0, opacityPaint); return adjustedBitmap; }
This was just a quick hack of a class; there is plenty of room for improvement. For example, I'm not sure the Bitmap to byte array stuff is solid or even the best optimization of that kind of conversion. Also, the tile image dimensions are hardcoded at 256 pixels, which is what Google Maps expects and what OWM serves. In my own code I've refactored to fix these shortcomings and make the class more flexible, and I encourage you to do the same and change whatever else to meet your own needs.
Hope this helps someone save a few hours of their life. If you spot any problems in the code send me an email.
















