Use android.graphic.canvas to create proper bitmap
This commit is contained in:
@@ -28,6 +28,7 @@ dependencies {
|
||||
api(projects.features.location.api)
|
||||
implementation(projects.features.messages.api)
|
||||
implementation(libs.maplibre.compose)
|
||||
implementation(libs.coil)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.di)
|
||||
|
||||
@@ -9,27 +9,20 @@ package io.element.android.features.location.impl.common.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.libraries.designsystem.components.LocationPin
|
||||
import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.components.rememberLocationPinBitmap
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import org.maplibre.compose.expressions.dsl.and
|
||||
import org.maplibre.compose.expressions.dsl.asNumber
|
||||
import org.maplibre.compose.expressions.dsl.asString
|
||||
import org.maplibre.compose.expressions.dsl.const
|
||||
import org.maplibre.compose.expressions.dsl.eq
|
||||
import org.maplibre.compose.expressions.dsl.feature
|
||||
import org.maplibre.compose.expressions.dsl.image
|
||||
import org.maplibre.compose.expressions.dsl.not
|
||||
import org.maplibre.compose.expressions.dsl.step
|
||||
import org.maplibre.compose.expressions.value.SymbolAnchor
|
||||
import org.maplibre.compose.expressions.value.SymbolPlacement
|
||||
import org.maplibre.compose.layers.CircleLayer
|
||||
import org.maplibre.compose.layers.SymbolLayer
|
||||
import org.maplibre.compose.sources.GeoJsonData
|
||||
@@ -60,14 +53,12 @@ data class LocationMarkerData(
|
||||
* A composable that renders location markers on a MapLibre map with clustering support.
|
||||
*
|
||||
* Uses GeoJSON source with clustering enabled to group nearby markers.
|
||||
* Individual markers are rendered using [LocationPin] composable converted to bitmaps.
|
||||
* Individual markers are rendered using Canvas-based pin rendering with Coil for avatar loading.
|
||||
* Clusters are rendered as circles with point counts.
|
||||
*
|
||||
* Must be used within a MaplibreMap content block.
|
||||
*
|
||||
* @param markers List of markers to display on the map
|
||||
* @param clusterRadius Radius of each cluster when clustering points (default 50)
|
||||
* @param clusterMaxZoom Maximum zoom level at which to cluster points (default 14)
|
||||
* @param onMarkerClick Callback when a marker is clicked
|
||||
* @param onClusterClick Callback when a cluster is clicked, provides cluster center position
|
||||
*/
|
||||
@@ -157,36 +148,23 @@ private fun LocationPinMarkerLayer(
|
||||
source: GeoJsonSource,
|
||||
onMarkerClick: ((LocationMarkerData) -> Unit)?,
|
||||
) {
|
||||
val imageBitmap = rememberLocationPinImage(marker.variant)
|
||||
SymbolLayer(
|
||||
id = "pin-marker-${marker.id}",
|
||||
source = source,
|
||||
filter = !feature.has("point_count") and (feature["id"].asString() eq const(marker.id)),
|
||||
iconImage = image(imageBitmap),
|
||||
iconAnchor = const(SymbolAnchor.Bottom),
|
||||
iconAllowOverlap = const(true),
|
||||
onClick = { features ->
|
||||
if (features.isNotEmpty() && onMarkerClick != null) {
|
||||
onMarkerClick(marker)
|
||||
ClickResult.Consume
|
||||
} else {
|
||||
ClickResult.Pass
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a LocationPin composable to an ImageBitmap for use in SymbolLayer.
|
||||
*/
|
||||
@Composable
|
||||
private fun rememberLocationPinImage(variant: PinVariant): ImageBitmap {
|
||||
val bitmap = rememberMarkerBitmap(variant) {
|
||||
LocationPin(
|
||||
variant = variant,
|
||||
// Disable as it doesn't work with the rememberMarkerBitmap method
|
||||
allowHardwareBitmapRendering = false
|
||||
val imageBitmap = rememberLocationPinBitmap(marker.variant)
|
||||
if (imageBitmap != null) {
|
||||
SymbolLayer(
|
||||
id = "pin-marker-${marker.id}",
|
||||
source = source,
|
||||
filter = !feature.has("point_count") and (feature["id"].asString() eq const(marker.id)),
|
||||
iconImage = image(imageBitmap),
|
||||
iconAnchor = const(SymbolAnchor.Bottom),
|
||||
iconAllowOverlap = const(true),
|
||||
onClick = { features ->
|
||||
if (features.isNotEmpty() && onMarkerClick != null) {
|
||||
onMarkerClick(marker)
|
||||
ClickResult.Consume
|
||||
} else {
|
||||
ClickResult.Pass
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
return bitmap.asImageBitmap()
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.location.impl.common.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionContext
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCompositionContext
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
/**
|
||||
* Renders a composable [content] to an Android [Bitmap].
|
||||
* Useful for MapLibre SymbolLayer rendering.
|
||||
*
|
||||
* Uses a temporary ComposeView to render off-screen without
|
||||
* adding to the visible composition tree.
|
||||
*
|
||||
* Note: This function provides a software-only ImageLoader to avoid
|
||||
* "Software rendering doesn't support hardware bitmaps" errors when
|
||||
* rendering Coil images to a Canvas.
|
||||
*
|
||||
* @param keys to trigger recomposition.
|
||||
* @return The rendered Android [Bitmap].
|
||||
*/
|
||||
@Composable
|
||||
fun rememberMarkerBitmap(
|
||||
vararg keys: Any,
|
||||
content: @Composable () -> Unit,
|
||||
): Bitmap {
|
||||
val parent = LocalView.current as ViewGroup
|
||||
val compositionContext = rememberCompositionContext()
|
||||
return remember(parent, compositionContext, *keys) {
|
||||
renderComposableToBitmap(parent, compositionContext, content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderComposableToBitmap(
|
||||
parent: ViewGroup,
|
||||
compositionContext: CompositionContext,
|
||||
content: @Composable () -> Unit,
|
||||
): Bitmap {
|
||||
val composeView = ComposeView(parent.context).apply {
|
||||
setParentCompositionContext(compositionContext)
|
||||
setContent(content)
|
||||
}
|
||||
// Temporarily add to parent for measurement
|
||||
parent.addView(
|
||||
composeView,
|
||||
ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
)
|
||||
// Measure
|
||||
composeView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
|
||||
val width = composeView.measuredWidth
|
||||
val height = composeView.measuredHeight
|
||||
|
||||
// Layout
|
||||
composeView.layout(0, 0, width, height)
|
||||
|
||||
// Draw to bitmap
|
||||
val bitmap = createBitmap(width, height)
|
||||
val canvas = Canvas(bitmap)
|
||||
composeView.draw(canvas)
|
||||
|
||||
// Cleanup
|
||||
parent.removeView(composeView)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
Reference in New Issue
Block a user