Start using LocationPinMarker in Share and Show locations

This commit is contained in:
ganfra 2026-03-03 22:05:17 +01:00
parent ec1d6ebabb
commit f4bf596e3b
3 changed files with 74 additions and 52 deletions

View file

@ -0,0 +1,37 @@
/*
* 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()
}
}

View file

@ -8,7 +8,6 @@
package io.element.android.features.location.impl.share package io.element.android.features.location.impl.share
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -28,7 +27,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -42,18 +40,20 @@ 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.LocationFloatingActionButton
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
import io.element.android.features.location.impl.common.ui.UserLocationPuck 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.AvatarSize
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ListDialog import io.element.android.libraries.designsystem.components.dialogs.ListDialog
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.list.RadioButtonListItem import io.element.android.libraries.designsystem.components.list.RadioButtonListItem
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import org.maplibre.compose.camera.CameraMoveReason import org.maplibre.compose.camera.CameraMoveReason
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
@ -155,10 +155,13 @@ fun ShareLocationView(
.fillMaxSize() .fillMaxSize()
.padding(sheetPadding) .padding(sheetPadding)
) { ) {
Icon( val variant = if (state.trackUserLocation) {
resourceId = CommonDrawables.pin, PinVariant.UserLocation(isLive = false, avatarData = state.currentUser.getAvatarData(AvatarSize.SelectedUser))
contentDescription = null, } else {
tint = Color.Unspecified, PinVariant.PinnedLocation
}
LocationPinMarker(
variant = variant,
modifier = Modifier.centerBottomEdge(this), modifier = Modifier.centerBottomEdge(this),
) )
} }

View file

@ -9,6 +9,7 @@
package io.element.android.features.location.impl.show package io.element.android.features.location.impl.show
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetValue import androidx.compose.material3.SheetValue
@ -18,22 +19,26 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp 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.CompoundIcons
import io.element.android.compound.tokens.generated.TypographyTokens import io.element.android.compound.tokens.generated.TypographyTokens
import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.R
import io.element.android.features.location.impl.common.MapDefaults 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.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog 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.LocationFloatingActionButton
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold 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.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
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -41,19 +46,15 @@ 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.IconButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar 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 import io.element.android.libraries.ui.strings.CommonStrings
import org.maplibre.compose.camera.CameraMoveReason import org.maplibre.compose.camera.CameraMoveReason
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.expressions.dsl.image
import org.maplibre.compose.layers.SymbolLayer
import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.rememberDefaultLocationProvider import org.maplibre.compose.location.rememberDefaultLocationProvider
import org.maplibre.compose.location.rememberNullLocationProvider import org.maplibre.compose.location.rememberNullLocationProvider
import org.maplibre.compose.location.rememberUserLocationState import org.maplibre.compose.location.rememberUserLocationState
import org.maplibre.compose.sources.GeoJsonData
import org.maplibre.compose.sources.rememberGeoJsonSource
import org.maplibre.spatialk.geojson.Point
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
@ -105,10 +106,9 @@ fun ShowLocationView(
} }
val scaffoldState = rememberBottomSheetScaffoldState( val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState() bottomSheetState = rememberStandardBottomSheetState(skipHiddenState = false, initialValue = SheetValue.Hidden)
) )
MapBottomSheetScaffold( MapBottomSheetScaffold(
sheetPeekHeight = 180.dp,
scaffoldState = scaffoldState, scaffoldState = scaffoldState,
cameraState = cameraState, cameraState = cameraState,
modifier = modifier, modifier = modifier,
@ -132,56 +132,38 @@ fun ShowLocationView(
} }
) )
}, },
sheetContent = {
when (val mode = state.mode) {
is ShowLocationMode.Static -> {
Text(
text = mode.senderName,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = TypographyTokens.fontBodyMdRegular,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
)
}
ShowLocationMode.Live -> {
// TODO: Show list of active live location sharers
}
}
},
mapContent = { mapContent = {
UserLocationPuck( UserLocationPuck(
cameraState = cameraState, cameraState = cameraState,
locationState = userLocationState, locationState = userLocationState,
trackUserLocation = state.isTrackMyLocation trackUserLocation = state.isTrackMyLocation
) )
},
overlayContent = {
when (val mode = state.mode) { when (val mode = state.mode) {
is ShowLocationMode.Static -> { is ShowLocationMode.Static -> {
val senderLocation = rememberGeoJsonSource( val pinVariant = if (mode.assetType == AssetType.PIN) {
data = GeoJsonData.Features( PinVariant.PinnedLocation
Point( } else {
Position( PinVariant.UserLocation(
latitude = mode.location.lat, avatarData = AvatarData(mode.senderId.value, mode.senderName, mode.senderAvatarUrl, AvatarSize.UserListItem),
longitude = mode.location.lon isLive = false
)
)
) )
}
val position = Position(
latitude = mode.location.lat,
longitude = mode.location.lon
) )
val marker = painterResource(R.drawable.pin_small) MapProjected(target = position, cameraState = cameraState) {
SymbolLayer( LocationPinMarker(variant = pinVariant)
id = "sender_location", }
source = senderLocation,
iconImage = image(marker)
)
} }
ShowLocationMode.Live -> { ShowLocationMode.Live -> {
// TODO: Show pins for all active live location sharers // TODO: Show pins for all active live location sharers
} }
} }
},
overlayContent = {
LocationFloatingActionButton( LocationFloatingActionButton(
isMapCenteredOnUser = state.isTrackMyLocation, isMapCenteredOnUser = state.isTrackMyLocation,
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) }, onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },