Introduce LocationPinMarkers composable
This commit is contained in:
@@ -19,7 +19,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -32,12 +31,10 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
||||
import io.element.android.features.location.api.internal.StaticMapUrlBuilder
|
||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||
import io.element.android.libraries.designsystem.components.LocationPinMarker
|
||||
import io.element.android.libraries.designsystem.components.LocationPin
|
||||
import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
|
||||
/**
|
||||
* Shows a static map image downloaded via a third party service's static maps API.
|
||||
@@ -98,7 +95,7 @@ fun StaticMapView(
|
||||
// We apply ContentScale.Fit to scale the image to fill the AsyncImage should this be the case.
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
LocationPinMarker(variant = pinVariant, modifier = Modifier.centerBottomEdge(this))
|
||||
LocationPin(variant = pinVariant, modifier = Modifier.centerBottomEdge(this))
|
||||
} else {
|
||||
StaticMapPlaceholder(
|
||||
showProgress = collectedState.value.isLoading(),
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 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 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
|
||||
import org.maplibre.compose.sources.GeoJsonOptions
|
||||
import org.maplibre.compose.sources.GeoJsonSource
|
||||
import org.maplibre.compose.sources.rememberGeoJsonSource
|
||||
import org.maplibre.compose.util.ClickResult
|
||||
import org.maplibre.spatialk.geojson.Feature
|
||||
import org.maplibre.spatialk.geojson.FeatureCollection
|
||||
import org.maplibre.spatialk.geojson.Point
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import org.maplibre.spatialk.geojson.toJson
|
||||
|
||||
/**
|
||||
* Data class representing a marker on the map.
|
||||
*
|
||||
* @param id Unique identifier for the marker
|
||||
* @param location The geographic location of the marker
|
||||
* @param variant The visual variant of the pin (user location, pinned, stale)
|
||||
*/
|
||||
data class LocationMarkerData(
|
||||
val id: String,
|
||||
val location: Location,
|
||||
val variant: PinVariant,
|
||||
)
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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
|
||||
*/
|
||||
@Composable
|
||||
fun LocationPinMarkers(
|
||||
markers: List<LocationMarkerData>,
|
||||
onMarkerClick: ((LocationMarkerData) -> Unit)? = null,
|
||||
onClusterClick: ((Position) -> Unit)? = null,
|
||||
) {
|
||||
if (markers.isEmpty()) return
|
||||
val clusterColor = ElementTheme.colors.bgAccentRest
|
||||
val clusterStrokeColor = ElementTheme.colors.iconOnSolidPrimary
|
||||
val clusterTextColor = ElementTheme.colors.textOnSolidPrimary
|
||||
val clusterTextStyle = ElementTheme.typography.fontBodyMdMedium
|
||||
|
||||
// Convert markers to GeoJSON
|
||||
val geoJsonString = remember(markers) {
|
||||
val features = markers.map { marker ->
|
||||
Feature(
|
||||
id = JsonPrimitive(marker.id),
|
||||
geometry = Point(Position(marker.location.lon, marker.location.lat)),
|
||||
properties = mapOf(
|
||||
"id" to JsonPrimitive(marker.id),
|
||||
)
|
||||
)
|
||||
}
|
||||
FeatureCollection(features).toJson()
|
||||
}
|
||||
|
||||
// Create GeoJSON source with clustering
|
||||
val markersSource = rememberGeoJsonSource(
|
||||
data = GeoJsonData.JsonString(geoJsonString),
|
||||
options = GeoJsonOptions(
|
||||
cluster = true,
|
||||
clusterMinPoints = 3,
|
||||
clusterRadius = 30
|
||||
),
|
||||
)
|
||||
|
||||
// Cluster circle layer
|
||||
CircleLayer(
|
||||
id = "cluster-circles",
|
||||
source = markersSource,
|
||||
filter = feature.has("point_count"),
|
||||
color = const(clusterColor),
|
||||
radius = const(24.dp),
|
||||
strokeWidth = const(1.dp),
|
||||
strokeColor = const(clusterStrokeColor),
|
||||
onClick = { features ->
|
||||
features.firstOrNull()?.let { feat ->
|
||||
val point = feat.geometry as? Point
|
||||
if (point != null && onClusterClick != null) {
|
||||
onClusterClick(point.coordinates)
|
||||
ClickResult.Consume
|
||||
} else {
|
||||
ClickResult.Pass
|
||||
}
|
||||
} ?: ClickResult.Pass
|
||||
},
|
||||
)
|
||||
|
||||
// Cluster count text layer
|
||||
SymbolLayer(
|
||||
id = "cluster-count",
|
||||
source = markersSource,
|
||||
filter = feature.has("point_count"),
|
||||
textField = feature["point_count_abbreviated"].asString(),
|
||||
textColor = const(clusterTextColor),
|
||||
textSize = const(clusterTextStyle.fontSize),
|
||||
textFont = const(listOfNotNull(clusterTextStyle.fontFamily?.toString())),
|
||||
textLetterSpacing = const(clusterTextStyle.letterSpacing),
|
||||
)
|
||||
|
||||
// Individual marker layers - one per marker for unique avatars
|
||||
markers.forEach { marker ->
|
||||
LocationPinMarkerLayer(
|
||||
marker = marker,
|
||||
source = markersSource,
|
||||
onMarkerClick = onMarkerClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocationPinMarkerLayer(
|
||||
marker: LocationMarkerData,
|
||||
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)
|
||||
}
|
||||
return bitmap.asImageBitmap()
|
||||
}
|
||||
@@ -1,37 +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 androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
|
||||
@Composable
|
||||
fun MapProjected(
|
||||
target: Position,
|
||||
cameraState: CameraState,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.graphicsLayer {
|
||||
cameraState.position
|
||||
val offset = cameraState.projection?.screenLocationFromPosition(target)
|
||||
if (offset != null) {
|
||||
translationX = offset.x.toPx() - size.width / 2
|
||||
translationY = offset.y.toPx() - size.height
|
||||
}
|
||||
}
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import io.element.android.features.location.impl.common.PermissionRationaleDialo
|
||||
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
|
||||
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
||||
import io.element.android.libraries.designsystem.components.LocationPinMarker
|
||||
import io.element.android.libraries.designsystem.components.LocationPin
|
||||
import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
@@ -160,7 +160,7 @@ fun ShareLocationView(
|
||||
} else {
|
||||
PinVariant.PinnedLocation
|
||||
}
|
||||
LocationPinMarker(
|
||||
LocationPin(
|
||||
variant = variant,
|
||||
modifier = Modifier.centerBottomEdge(this),
|
||||
)
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
package io.element.android.features.location.impl.show
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
@@ -17,25 +15,23 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.compound.tokens.generated.TypographyTokens
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.ShowLocationMode
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.features.location.impl.common.PermissionDeniedDialog
|
||||
import io.element.android.features.location.impl.common.PermissionRationaleDialog
|
||||
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
|
||||
import io.element.android.features.location.impl.common.ui.LocationMarkerData
|
||||
import io.element.android.features.location.impl.common.ui.LocationPinMarkers
|
||||
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.MapProjected
|
||||
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
||||
import io.element.android.libraries.designsystem.components.LocationPinMarker
|
||||
import io.element.android.libraries.designsystem.components.PinVariant
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
@@ -44,7 +40,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@@ -56,6 +51,10 @@ import org.maplibre.compose.location.rememberDefaultLocationProvider
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
import org.maplibre.compose.location.rememberUserLocationState
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -138,8 +137,6 @@ fun ShowLocationView(
|
||||
locationState = userLocationState,
|
||||
trackUserLocation = state.isTrackMyLocation
|
||||
)
|
||||
},
|
||||
overlayContent = {
|
||||
when (val mode = state.mode) {
|
||||
is ShowLocationMode.Static -> {
|
||||
val pinVariant = if (mode.assetType == AssetType.PIN) {
|
||||
@@ -147,23 +144,64 @@ fun ShowLocationView(
|
||||
} else {
|
||||
PinVariant.UserLocation(
|
||||
avatarData = AvatarData(mode.senderId.value, mode.senderName, mode.senderAvatarUrl, AvatarSize.UserListItem),
|
||||
isLive = false
|
||||
isLive = true
|
||||
)
|
||||
}
|
||||
val position = Position(
|
||||
latitude = mode.location.lat,
|
||||
longitude = mode.location.lon
|
||||
)
|
||||
MapProjected(target = position, cameraState = cameraState) {
|
||||
LocationPinMarker(variant = pinVariant)
|
||||
// Generate test markers around the original location
|
||||
val testMarkers = remember {
|
||||
buildList {
|
||||
// Add the original marker
|
||||
add(
|
||||
LocationMarkerData(
|
||||
id = "original",
|
||||
location = mode.location,
|
||||
variant = pinVariant
|
||||
)
|
||||
)
|
||||
// Generate 10 random points within 50 meters
|
||||
val radiusInMeters = 50.0
|
||||
val metersPerDegreeLat = 111_320.0
|
||||
val metersPerDegreeLon = 111_320.0 * cos(Math.toRadians(mode.location.lat))
|
||||
val variants = listOf(
|
||||
PinVariant.StaleLocation,
|
||||
PinVariant.UserLocation(AvatarData("@alice", "Alice", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@bob", "Bob", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@cassy", "Cassy", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@daisy", "Daisy", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@en", "G", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@f", "H", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@g", "I", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@h", "J", null, AvatarSize.TimelineSender), isLive = true),
|
||||
PinVariant.UserLocation(AvatarData("@i", "K", null, AvatarSize.TimelineSender), isLive = true),
|
||||
)
|
||||
repeat(10) { index ->
|
||||
// Random point in a circle using sqrt for uniform distribution
|
||||
val angle = Random.nextDouble() * 2 * Math.PI
|
||||
val distance = sqrt(Random.nextDouble()) * radiusInMeters
|
||||
val latOffset = (distance * cos(angle)) / metersPerDegreeLat
|
||||
val lonOffset = (distance * sin(angle)) / metersPerDegreeLon
|
||||
add(
|
||||
LocationMarkerData(
|
||||
id = "test_$index",
|
||||
location = Location(
|
||||
lat = mode.location.lat + latOffset,
|
||||
lon = mode.location.lon + lonOffset
|
||||
),
|
||||
variant = variants[index % (variants.size-1)]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LocationPinMarkers(testMarkers)
|
||||
}
|
||||
ShowLocationMode.Live -> {
|
||||
// TODO: Show pins for all active live location sharers
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
overlayContent = {
|
||||
LocationFloatingActionButton(
|
||||
isMapCenteredOnUser = state.isTrackMyLocation,
|
||||
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
||||
|
||||
@@ -53,12 +53,12 @@ private val DOT_RADIUS = 6.dp
|
||||
private val CONTENT_OFFSET = 5.dp
|
||||
|
||||
/**
|
||||
* A location pin marker composable that supports multiple variants.
|
||||
* A location pin composable that supports multiple variants.
|
||||
*
|
||||
* Based on Figma design: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=4665-2890&m=dev
|
||||
*/
|
||||
@Composable
|
||||
fun LocationPinMarker(
|
||||
fun LocationPin(
|
||||
variant: PinVariant,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -195,7 +195,7 @@ private fun DrawScope.drawPinShape(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LocationPinMarkerPreview() = ElementPreview {
|
||||
internal fun LocationPinPreview() = ElementPreview {
|
||||
val sampleAvatarData = AvatarData(
|
||||
id = "@alice:matrix.org",
|
||||
name = "Alice",
|
||||
@@ -208,18 +208,18 @@ internal fun LocationPinMarkerPreview() = ElementPreview {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
LocationPinMarker(
|
||||
LocationPin(
|
||||
variant = PinVariant.UserLocation(avatarData = sampleAvatarData, isLive = false),
|
||||
)
|
||||
LocationPinMarker(
|
||||
LocationPin(
|
||||
variant = PinVariant.UserLocation(avatarData = sampleAvatarData, isLive = true),
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
LocationPinMarker(
|
||||
LocationPin(
|
||||
variant = PinVariant.PinnedLocation,
|
||||
)
|
||||
LocationPinMarker(
|
||||
LocationPin(
|
||||
variant = PinVariant.StaleLocation,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user