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:
{
"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.