Skip to Content
AdvancedCustom Icons

MapsGL Android - Custom Icons

With symbol layers, you can use icons to represent point features on the map. You can set the icon for the whole layer, or choose it per feature with data-driven styling.

This guide covers the ways to use icons with SymbolLayerDescriptor layers through the IconPaint.image paint property.

Built-in and Custom Icons

Symbol layers are drawn by MapsGL’s own renderer by default. That renderer draws icons from the MapsGL sprite sheet, which the SDK loads for you. Its icons have ids that start with mapsgl:, such as mapsgl:fire-1. The full list of names is in the sprite’s JSON index .

import com.xweather.mapsgl.layers.spec.SymbolLayerDescriptor import com.xweather.mapsgl.style.IconPaint import com.xweather.mapsgl.style.StyleValue import com.xweather.mapsgl.style.SymbolLayerPaint controller.addLayer( SymbolLayerDescriptor( id = "fires", source = "my-source", paint = SymbolLayerPaint( icon = IconPaint(image = StyleValue.Constant("mapsgl:fire-1")), ), ), beforeID = null, )

To use your own images, register each one on the Mapbox style, and set useGlRendering = false on the layer. Mapbox then draws the layer as a style layer, using the images registered on its style:

val icon = BitmapFactory.decodeResource(resources, R.drawable.my_icon) controller.mapboxMap?.getStyle { style -> style.addImage("my-icon", icon, false) } controller.addLayer( SymbolLayerDescriptor( id = "my-layer", source = "my-source", paint = SymbolLayerPaint( icon = IconPaint(image = StyleValue.Constant("my-icon")), ), useGlRendering = false, ), beforeID = null, )

A layer drawn by Mapbox doesn’t support MapsGL renderer features such as custom icon shaders.

Using Sprites

Sprites , also known as sprite sheets, image sprites, or texture atlases, are a collection of images put into a single image. They reduce the number of image requests when you have many icons.

Generating a Sprite Sheet

There are a number of tools for generating sprite sheets. One popular command-line tool is spreet , which generates a sprite sheet from a directory of SVG images. It also generates a JSON index that gives each image’s name and position in the sheet:

sprite.json
{ "partly-cloudy": { "x": 0, "y": 0, "width": 256, "height": 256, "pixelRatio": 2 }, "rain": { "x": 0, "y": 256, "width": 256, "height": 256, "pixelRatio": 2 }, "snow": { "x": 256, "y": 0, "width": 256, "height": 256, "pixelRatio": 2 }, "storms": { "x": 256, "y": 256, "width": 256, "height": 256, "pixelRatio": 2 }, "sunny": { "x": 512, "y": 0, "width": 256, "height": 256, "pixelRatio": 2 } }

Loading a Sprite Sheet

The Android SDK has no method for loading your own sprite sheet. Instead, decode the sheet, cut out each icon using the JSON index, and register each icon on the Mapbox style. Prefix the names with an id of your own so they don’t clash with other images:

import android.graphics.Bitmap import android.graphics.BitmapFactory import org.json.JSONObject fun registerSprite(prefix: String, sheet: Bitmap, indexJson: String) { val index = JSONObject(indexJson) controller.mapboxMap?.getStyle { style -> for (name in index.keys()) { val rect = index.getJSONObject(name) val icon = Bitmap.createBitmap( sheet, rect.getInt("x"), rect.getInt("y"), rect.getInt("width"), rect.getInt("height"), ) style.addImage("$prefix:$name", icon, false) } } }

Load the PNG and JSON from your app’s assets, or download them off the main thread, then call registerSprite("my-sprite", sheet, json).

Using Icons from a Sprite Sheet

Reference an icon by its prefixed name in IconPaint.image:

controller.addLayer( SymbolLayerDescriptor( id = "my-layer", source = "my-source", paint = SymbolLayerPaint( icon = IconPaint(image = StyleValue.Constant("my-sprite:sunny")), ), useGlRendering = false, ), beforeID = null, )

You can register more than one sprite sheet. Use a different prefix for each.

Adding Remote Images

To use a single image from a URL, download and decode it off the main thread, then register it the same way:

import java.net.URL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext lifecycleScope.launch { val icon = withContext(Dispatchers.IO) { URL("https://example.com/icon.png").openStream().use { BitmapFactory.decodeStream(it) } } controller.mapboxMap?.getStyle { style -> style.addImage("my-icon", icon, false) } }

Add the layer that uses the image after it is registered.

Adding Images

You can also draw an image in code with Android’s Canvas and register the resulting bitmap:

import android.graphics.Canvas import android.graphics.Paint val size = 36 val lineWidth = 2f val icon = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) val canvas = Canvas(icon) val radius = size / 2f - lineWidth canvas.drawCircle(size / 2f, size / 2f, radius, Paint().apply { color = android.graphics.Color.argb(77, 255, 0, 0) isAntiAlias = true }) canvas.drawCircle(size / 2f, size / 2f, radius, Paint().apply { color = android.graphics.Color.RED style = Paint.Style.STROKE strokeWidth = lineWidth isAntiAlias = true }) controller.mapboxMap?.getStyle { style -> style.addImage("my-icon", icon, false) }

Animated Icons

The JavaScript SDK can redraw an icon image on every frame. On Android, animate icons with a custom GLSL fragment shader on IconPaint.shader instead. The shader runs for every icon on every frame, on layers drawn by MapsGL’s renderer.

Data-Driven Icons

You can choose the icon for each feature from its properties with an expression.

For example, if you’re showing current weather observations, you can pick an icon from the sprite sheet above for each observation’s weather. This uses Expression.match on a feature’s weather property:

import com.xweather.mapsgl.style.Expression controller.addLayer( SymbolLayerDescriptor( id = "obs", source = "obs", paint = SymbolLayerPaint( icon = IconPaint( image = StyleValue.Expression( Expression.match( Expression.get("weather"), listOf( Expression.Step("partly-cloudy", "my-sprite:partly-cloudy"), Expression.Step("rain", "my-sprite:rain"), Expression.Step("snow", "my-sprite:snow"), Expression.Step("storms", "my-sprite:storms"), ), "my-sprite:sunny", ), ), ), ), useGlRendering = false, ), beforeID = null, )

The JavaScript SDK can also use a function to pick an icon. On Android, use expressions.

SDF Icons

If you want the same icon for every feature but with a different color per feature, use signed distance field  (SDF) icons. An SDF icon is a single-color icon that is recolored when it is drawn, using fill.color on the layer’s paint. SDF icons can also be scaled without losing sharpness.

Generating SDF icons is beyond the scope of this documentation, but there are tools and libraries  for creating them. Register an SDF icon by passing true as the last argument to addImage:

controller.mapboxMap?.getStyle { style -> style.addImage("lightning-icon", lightningBitmap, true) }

Then set the fill color, here by each feature’s age property in seconds:

import androidx.compose.ui.graphics.Color import com.xweather.mapsgl.style.FillPaint controller.addLayer( SymbolLayerDescriptor( id = "lightning", source = "lightning", paint = SymbolLayerPaint( icon = IconPaint(image = StyleValue.Constant("lightning-icon")), fill = FillPaint( color = StyleValue.Expression( Expression.switchCase( listOf( Expression.Case(Expression.greaterThanOrEqual(Expression.get("age"), 600), "#C4150D"), Expression.Case(Expression.greaterThanOrEqual(Expression.get("age"), 300), "#F97002"), Expression.Case(Expression.greaterThanOrEqual(Expression.get("age"), 120), "#F9E203"), ), "#EEEEEE", ), ), ), ), useGlRendering = false, ), beforeID = null, )

To restyle the built-in lightning layers instead, see the custom lightning styles example.

© 2026 Xweather (opens in a new tab)Terms of Service (opens in a new tab)Privacy Policy (opens in a new tab)