Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Drawing on GoogleMaps with Jetpack Compose

Drawing on GoogleMaps with Jetpack Compose

Slides for my talk at Droidcon Berlin 2024

Luca Nicoletti

July 04, 2024
Tweet

More Decks by Luca Nicoletti

Other Decks in Programming

Transcript

  1. Libraries Android Map Compose • GoogleMap • Control and con

    f iguration • Drawing • Markers & Marker Windows • Circles • Polygons/Polylines • Overlays Maps Compose Utility Library • Clustering
  2. Getting starter @Composable public fun GoogleMap( mergeDescendants: Boolean = false,

    modifier: Modifier = Modifier, cameraPositionState: CameraPositionState = rememberCameraPositionState(), contentDescription: String? = null, googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() }, properties: MapProperties = DefaultMapProperties, locationSource: LocationSource? = null, uiSettings: MapUiSettings = DefaultMapUiSettings, indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, onMapClick: ((LatLng) -> Unit)? = null, onMapLongClick: ((LatLng) -> Unit)? = null, onMapLoaded: (() -> Unit)? = null, onMyLocationButtonClick: (() -> Boolean)? = null, onMyLocationClick: ((Location) -> Unit)? = null, onPOIClick: ((PointOfInterest) -> Unit)? = null, contentPadding: PaddingValues = NoPadding, content: (@Composable @GoogleMapComposable () -> Unit)? = null, )
  3. Getting starter @Composable public fun GoogleMap( mergeDescendants: Boolean = false,

    modifier: Modifier = Modifier, cameraPositionState: CameraPositionState = rememberCameraPositionState(), contentDescription: String? = null, googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() }, properties: MapProperties = DefaultMapProperties, locationSource: LocationSource? = null, uiSettings: MapUiSettings = DefaultMapUiSettings, indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, onMapClick: ((LatLng) -> Unit)? = null, onMapLongClick: ((LatLng) -> Unit)? = null, onMapLoaded: (() -> Unit)? = null, onMyLocationButtonClick: (() -> Boolean)? = null, onMyLocationClick: ((Location) -> Unit)? = null, onPOIClick: ((PointOfInterest) -> Unit)? = null, contentPadding: PaddingValues = NoPadding, content: (@Composable @GoogleMapComposable () -> Unit)? = null, )
  4. Getting starter @Composable public fun GoogleMap( mergeDescendants: Boolean = false,

    modifier: Modifier = Modifier, cameraPositionState: CameraPositionState = rememberCameraPositionState(), contentDescription: String? = null, googleMapOptionsFactory: () -> GoogleMapOptions = { GoogleMapOptions() }, properties: MapProperties = DefaultMapProperties, locationSource: LocationSource? = null, uiSettings: MapUiSettings = DefaultMapUiSettings, indoorStateChangeListener: IndoorStateChangeListener = DefaultIndoorStateChangeListener, onMapClick: ((LatLng) -> Unit)? = null, onMapLongClick: ((LatLng) -> Unit)? = null, onMapLoaded: (() -> Unit)? = null, onMyLocationButtonClick: (() -> Boolean)? = null, onMyLocationClick: ((Location) -> Unit)? = null, onPOIClick: ((PointOfInterest) -> Unit)? = null, contentPadding: PaddingValues = NoPadding, content: (@Composable @GoogleMapComposable () -> Unit)? = null, )
  5. Getting starter public class MapProperties( public val isBuildingEnabled: Boolean =

    false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, public val isTrafficEnabled: Boolean = false, public val latLngBoundsForCameraTarget: LatLngBounds? = null, public val mapStyleOptions: MapStyleOptions? = null, public val mapType: MapType = MapType.NORMAL, public val maxZoomPreference: Float = 21.0f, public val minZoomPreference: Float = 3.0f, )
  6. Getting starter public class MapProperties( public val isBuildingEnabled: Boolean =

    false, public val isIndoorEnabled: Boolean = false, public val isMyLocationEnabled: Boolean = false, public val isTrafficEnabled: Boolean = false, public val latLngBoundsForCameraTarget: LatLngBounds? = null, public val mapStyleOptions: MapStyleOptions? = null, public val mapType: MapType = MapType.NORMAL, public val maxZoomPreference: Float = 21.0f, public val minZoomPreference: Float = 3.0f, )
  7. MapStyleOptions What can be customised • Administrative • Country •

    Province • Locality • Neighbourhood • Land parcel • Landscape • Human-made • Natural • Landcover • Terrain • Points of interest • Attraction • Business • Government • Medical • Park • Place of worship • School • Sports Complex • Road • Highway • Controlled access • Arterial • Local • Transit • Line • Station • Airport • Bus • Rail • Water
  8. MapStyleOptions • Geometry • Fill • Stroke • Labels •

    Text • Text f ill • Text outline • Icon
  9. Getting starter public class MapUiSettings( public val compassEnabled: Boolean =

    true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, public val zoomControlsEnabled: Boolean = true, public val zoomGesturesEnabled: Boolean = true, )
  10. Getting starter public class MapUiSettings( public val compassEnabled: Boolean =

    true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, public val zoomControlsEnabled: Boolean = true, public val zoomGesturesEnabled: Boolean = true, )
  11. Getting starter public class MapUiSettings( public val compassEnabled: Boolean =

    true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, public val zoomControlsEnabled: Boolean = true, public val zoomGesturesEnabled: Boolean = true, )
  12. Getting starter public class MapUiSettings( public val compassEnabled: Boolean =

    true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, public val zoomControlsEnabled: Boolean = true, public val zoomGesturesEnabled: Boolean = true, )
  13. Getting starter public class MapUiSettings( public val compassEnabled: Boolean =

    true, public val indoorLevelPickerEnabled: Boolean = true, public val mapToolbarEnabled: Boolean = true, public val myLocationButtonEnabled: Boolean = true, public val rotationGesturesEnabled: Boolean = true, public val scrollGesturesEnabled: Boolean = true, public val scrollGesturesEnabledDuringRotateOrZoom: Boolean = true, public val tiltGesturesEnabled: Boolean = true, public val zoomControlsEnabled: Boolean = true, public val zoomGesturesEnabled: Boolean = true, )
  14. Marker @Composable @GoogleMapComposable public fun Marker( contentDescription: String? = "",

    state: MarkerState = rememberMarkerState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, flat: Boolean = false, icon: BitmapDescriptor? = null, infoWindowAnchor: Offset = Offset(0.5f, 0.0f), rotation: Float = 0.0f, snippet: String? = null, tag: Any? = null, title: String? = null, visible: Boolean = true, zIndex: Float = 0.0f, onClick: (Marker) -> Boolean = { false }, onInfoWindowClick: (Marker) -> Unit = {}, onInfoWindowClose: (Marker) -> Unit = {}, onInfoWindowLongClick: (Marker) -> Unit = {}, )
  15. Marker public static final float HUE_RED = 0.0F; public static

    final float HUE_ORANGE = 30.0F; public static final float HUE_YELLOW = 60.0F; public static final float HUE_GREEN = 120.0F; public static final float HUE_CYAN = 180.0F; public static final float HUE_AZURE = 210.0F; public static final float HUE_BLUE = 240.0F; public static final float HUE_VIOLET = 270.0F; public static final float HUE_MAGENTA = 300.0F; public static final float HUE_ROSE = 330.0F; Default Marker - HUE variations
  16. Marker @Composable @GoogleMapComposable public fun Marker( contentDescription: String? = "",

    state: MarkerState = rememberMarkerState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, flat: Boolean = false, icon: BitmapDescriptor? = null, infoWindowAnchor: Offset = Offset(0.5f, 0.0f), rotation: Float = 0.0f, snippet: String? = null, tag: Any? = null, title: String? = null, visible: Boolean = true, zIndex: Float = 0.0f, onClick: (Marker) -> Boolean = { false }, onInfoWindowClick: (Marker) -> Unit = {}, onInfoWindowClose: (Marker) -> Unit = {}, onInfoWindowLongClick: (Marker) -> Unit = {}, )
  17. Marker private fun bitmapDescriptor( context: Context, vectorResId: Int ): BitmapDescriptor?

    { val drawable = ContextCompat.getDrawable(context, vectorResId) ?: return null drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) val bm = Bitmap.createBitmap( /* width = */ drawable.intrinsicWidth, /* height = */ drawable.intrinsicHeight, /* config = */ Bitmap.Config.ARGB_8888 ) // draw it onto the bitmap val canvas = android.graphics.Canvas(bm) drawable.draw(canvas) return BitmapDescriptorFactory.fromBitmap(bm) }
  18. Marker @Composable fun MapMarker( position: LatLng, @DrawableRes iconResourceId: Int )

    { val context = LocalContext.current val icon = remember { bitmapDescriptor(context, iconResourceId) } Marker( state = MarkerState(position = position), icon = icon, anchor = Offset(HALF_OFFSET, HALF_OFFSET) ) } private const val HALF_OFFSET = 0.5f
  19. Marker Info Window @Composable @GoogleMapComposable public fun MarkerInfoWindow( state: MarkerState

    = rememberMarkerState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, flat: Boolean = false, icon: BitmapDescriptor? = null, infoWindowAnchor: Offset = Offset(0.5f, 0.0f), rotation: Float = 0.0f, snippet: String? = null, tag: Any? = null, title: String? = null, visible: Boolean = true, zIndex: Float = 0.0f, onClick: (Marker) -> Boolean = { false }, onInfoWindowClick: (Marker) -> Unit = {}, onInfoWindowClose: (Marker) -> Unit = {}, onInfoWindowLongClick: (Marker) -> Unit = {}, content: (@Composable (Marker) -> Unit)? = null )
  20. Marker Info Window MarkerInfoWindowContent( state = MarkerState(position = data.location), title

    = data.title, snippet = data.description, ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().padding(32.dp) ) { Text( modifier = Modifier.padding(top = 6.dp), text = data.title, fontWeight = FontWeight.Bold, color = Color.Black, ) data.description?.let { desc -> Text(desc) } data.imageResourceId?.let { res -> Image( modifier = Modifier.padding(top = 6.dp).size(240.dp), painter = painterResource(id = res), contentDescription = data.description ) } } }
  21. Cluster Item public interface ClusterItem { /** * The position

    of this marker. This must always return the same value. */ @NonNull LatLng getPosition(); /** * The title of this marker. */ @Nullable String getTitle(); /** * The description of this marker. */ @Nullable String getSnippet(); /** * The z-index of this marker. */ @Nullable Float getZIndex(); }
  22. Cluster Item data class MarkerData( val location: LatLng, val name:

    String, val description: String?, ) : ClusterItem { override fun getPosition(): LatLng = location override fun getTitle(): String? = name override fun getSnippet(): String? = description override fun getZIndex(): Float? = 1f }
  23. Clustering @Composable @GoogleMapComposable @MapsComposeExperimentalApi public fun <T : ClusterItem> Clustering(

    items: Collection<T>, onClusterClick: (Cluster<T>) -> Boolean = { false }, onClusterItemClick: (T) -> Boolean = { false }, onClusterItemInfoWindowClick: (T) -> Unit = { }, onClusterItemInfoWindowLongClick: (T) -> Unit = { }, clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null, clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null, )
  24. Clustering Clustering( items = markersData, clusterItemContent = { markerData ->

    val state = rememberMarkerState(position = markerData.position) Marker(state = state) }, )
  25. Clustering @Composable @GoogleMapComposable @MapsComposeExperimentalApi public fun <T : ClusterItem> Clustering(

    items: Collection<T>, onClusterClick: (Cluster<T>) -> Boolean = { false }, onClusterItemClick: (T) -> Boolean = { false }, onClusterItemInfoWindowClick: (T) -> Unit = { }, onClusterItemInfoWindowLongClick: (T) -> Unit = { }, clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null, clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null, )
  26. Circles @Composable @GoogleMapComposable public fun Circle( center: LatLng, clickable: Boolean

    = false, fillColor: Color = Color.Transparent, radius: Double = 0.0, strokeColor: Color = Color.Black, strokePattern: List<PatternItem>? = null, strokeWidth: Float = 10f, tag: Any? = null, visible: Boolean = true, zIndex: Float = 0f, onClick: (Circle) -> Unit = {}, )
  27. Circles @Composable @GoogleMapComposable public fun Circle( center: LatLng, clickable: Boolean

    = false, fillColor: Color = Color.Transparent, radius: Double = 0.0, strokeColor: Color = Color.Black, strokePattern: List<PatternItem>? = null, strokeWidth: Float = 10f, tag: Any? = null, visible: Boolean = true, zIndex: Float = 0f, onClick: (Circle) -> Unit = {}, )
  28. Circles @Composable @GoogleMapComposable public fun Circle( center: LatLng, clickable: Boolean

    = false, fillColor: Color = Color.Transparent, radius: Double = 0.0, strokeColor: Color = Color.Black, strokePattern: List<PatternItem>? = null, strokeWidth: Float = 10f, tag: Any? = null, visible: Boolean = true, zIndex: Float = 0f, onClick: (Circle) -> Unit = {}, )
  29. Circles Circle( center = LatLng(51.510949, -0.086413), fillColor = Color.Red, radius

    = 250.0, strokeColor = Color.Blue, strokeWidth = 25f, strokePattern = listOf(Dash(200f), Gap(1f), Dot(), Gap(1f)), )
  30. Circles Circle( center = LatLng(51.510949, -0.086413), fillColor = Color.Blue.copy(alpha =

    0.5f), radius = 250.0, strokeColor = Color.Transparent, strokeWidth = 0f, )
  31. Polygons @Composable @GoogleMapComposable public fun Polygon( points: List<LatLng>, clickable: Boolean

    = false, fillColor: Color = Color.Black, geodesic: Boolean = false, holes: List<List<LatLng>> = emptyList(), strokeColor: Color = Color.Black, strokeJointType: Int = JointType.DEFAULT, strokePattern: List<PatternItem>? = null, strokeWidth: Float = 10f, tag: Any? = null, visible: Boolean = true, zIndex: Float = 0f, onClick: (Polygon) -> Unit = {} )
  32. Polygons private val pins = listOf( LatLng(51.51223, -0.08317), LatLng(51.517619, -0.082958),

    LatLng(51.525481, -0.087196), LatLng(51.510949, -0.086413), ) Polygon(points = pins)
  33. Polygons private val pins = listOf( LatLng(51.51223, -0.08317), LatLng(51.517619, -0.082958),

    LatLng(51.525481, -0.087196), LatLng(51.510949, -0.086413), ) Polygon( points = pins, fillColor = Color.Cyan, strokeWidth = 3f, )
  34. Polygons Polygon( points = pins, fillColor = Color.Cyan, strokeWidth =

    15f, strokeColor = Color.Gray, strokePattern = listOf(Dash(50f), Gap(15f), Dot(), Gap(15f)) )
  35. Polygons Polygon( points = pins, fillColor = Color.Cyan.copy(alpha = 0.4f),

    strokeWidth = 15f, strokeColor = Color.Gray, strokePattern = listOf(Dash(50f), Gap(15f), Dot(), Gap(15f)) )
  36. Polygons Polygon( points = outerPins, fillColor = Color.Cyan.copy(alpha = 0.4f),

    strokeWidth = 15f, strokeColor = Color.Gray, strokePattern = listOf(Dash(50f), Gap(15f), Dot(), Gap(15f)), holes = listOf(pins) )
  37. Polygons Polygon( points = outerPins, fillColor = Color.Cyan.copy(alpha = 0.4f),

    strokeWidth = 15f, strokeColor = Color.Gray, strokePattern = listOf(Dash(50f), Gap(15f), Dot(), Gap(15f)), holes = listOf(pins, pins1) )
  38. Polylines @Composable @GoogleMapComposable public fun Polyline( points: List<LatLng>, clickable: Boolean

    = false, color: Color = Color.Black, endCap: Cap = ButtCap(), geodesic: Boolean = false, jointType: Int = JointType.DEFAULT, pattern: List<PatternItem>? = null, startCap: Cap = ButtCap(), tag: Any? = null, visible: Boolean = true, width: Float = 10f, zIndex: Float = 0f, onClick: (Polyline) -> Unit = {} )
  39. Polylines Polyline( points = outerPins, color = Color.Magenta, startCap =

    RoundCap(), endCap = SquareCap(), pattern = listOf(Dash(20f), Gap(20f)), )
  40. Polylines Polyline( points = markersData.map { it.position }, spans =

    listOf( StyleSpan(Color.Red.toArgb()), StyleSpan(Color.Blue.toArgb()) ) )
  41. Polylines Polyline( points = markersData.map { it.position }, spans =

    listOf( StyleSpan( StrokeStyle.gradientBuilder( Color.Red.toArgb(), Color.Yellow.toArgb(), ).build() ) ), width = 15f, endCap = RoundCap(), startCap = RoundCap(), )
  42. GroundOverlay @Composable @GoogleMapComposable public fun GroundOverlay( position: GroundOverlayPosition, image: BitmapDescriptor,

    anchor: Offset = Offset(0.5f, 0.5f), bearing: Float = 0f, clickable: Boolean = false, tag: Any? = null, transparency: Float = 0f, visible: Boolean = true, zIndex: Float = 0f, onClick: (GroundOverlay) -> Unit = {}, )
  43. GroundOverlay public fun create(latLngBounds: LatLngBounds) : GroundOverlayPosition { return GroundOverlayPosition(latLngBounds

    = latLngBounds) } public fun create(location: LatLng, width: Float, height: Float? = null) : 
 GroundOverlayPosition { return GroundOverlayPosition( location = location, width = width, height = height )
 }
  44. GroundOverlay @Composable fun GroundOverlayUnderground() { val context = LocalContext.current GroundOverlay(

    position = GroundOverlayPosition.create( latLngBounds = LatLngBounds( /* southwest = */ LatLng(51.493684, -0.224643), /* northeast = */ LatLng(51.527323, -0.056233), ) ), anchor = Offset.Zero, image = drawableToBitmapDescriptor( context, R.drawable.overlay1 ), transparency = 0.5f, ) }
  45. GroundOverlay fun drawableToBitmapDescriptor(context: Context, drawableId: Int): BitmapDescriptor { val drawableResource:

    Drawable? = ContextCompat.getDrawable(context, drawableId) drawableResource?.let { drawable -> val width = drawable.intrinsicWidth val height = drawable.intrinsicHeight val bitmap: Bitmap = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 ) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) canvas.scale(15f, 15f) return BitmapDescriptorFactory.fromBitmap(bitmap) } ?: run { throw IllegalArgumentException("Drawable not found") } }
  46. TileOverlay @Composable @GoogleMapComposable public fun TileOverlay( tileProvider: TileProvider, state: TileOverlayState

    = rememberTileOverlayState(), fadeIn: Boolean = true, transparency: Float = 0f, visible: Boolean = true, zIndex: Float = 0f, onClick: (TileOverlay) -> Unit = {}, )
  47. TileOverlay public interface TileProvider { @NonNull Tile NO_TILE = new

    Tile(-1, -1, (byte[])null); @Nullable Tile getTile(int var1, int var2, int var3); }
  48. TileOverlay class CoordTileProvider(context: Context) : TileProvider { private val scaleFactor:

    Float = context.resources.displayMetrics.density * 0.6f private val borderTile: Bitmap companion object { private const val TILE_SIZE_DP = 256 } init { val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG) borderPaint.style = Paint.Style.STROKE borderTile = Bitmap.createBitmap( (TILE_SIZE_DP * scaleFactor).toInt(), (TILE_SIZE_DP * scaleFactor).toInt(), Bitmap.Config.ARGB_8888 ) val canvas = Canvas(borderTile) canvas.drawRect( 0f, 0f, TILE_SIZE_DP * scaleFactor, TILE_SIZE_DP * scaleFactor, borderPaint ) } }
  49. TileOverlay override fun getTile(x: Int, y: Int, zoom: Int): Tile

    { val coordTile = drawTileCoords(x, y, zoom) val stream = ByteArrayOutputStream() coordTile!!.compress(Bitmap.CompressFormat.PNG, 0, stream) val bitmapData = stream.toByteArray() return Tile( (TILE_SIZE_DP * scaleFactor).toInt(), (TILE_SIZE_DP * scaleFactor).toInt(), bitmapData ) }
  50. TileOverlay private fun drawTileCoords(x: Int, y: Int, zoom: Int): Bitmap?

    { var copy: Bitmap? synchronized(borderTile) { 
 copy = borderTile.copy(Bitmap.Config.ARGB_8888, true) 
 } val canvas = Canvas(copy!!) val tileCoords = "($x, $y)" val zoomLevel = "zoom = $zoom" val mTextPaint = Paint(Paint.ANTI_ALIAS_FLAG) mTextPaint.color = Color.Blue.toArgb() mTextPaint.textAlign = Paint.Align.CENTER mTextPaint.textSize = 18 * scaleFactor canvas.drawText( tileCoords, TILE_SIZE_DP * scaleFactor / 2, TILE_SIZE_DP * scaleFactor / 2, mTextPaint ) canvas.drawText( zoomLevel, TILE_SIZE_DP * scaleFactor / 2, TILE_SIZE_DP * scaleFactor * 2 / 3, mTextPaint ) return copy }
  51. TileOverlay • The tile at (0,0) is at the northwest

    corner of the map • At zoom level 0, the entire world is rendered in a single tile • At zoom level 1 the map will be rendered as a 2x2 grid of tiles • # tiles = (2 ^ zoom level) x (2 ^ zoom level)
  52. TileOverlay class MapTileProvider(tileSize: Int) : UrlTileProvider(tileSize, tileSize) { private val

    urlTemplate = "https://" + BuildConfig.TILE_WEB_URL + "/%d/%d/%d.jpg?key=" + BuildConfig.TILE_API_KEY override fun getTileUrl(x: Int, y: Int, zoom: Int): URL = URL(urlTemplate.format(zoom, x, y)) }