First iteration using maplibre-compose
This commit is contained in:
@@ -27,7 +27,7 @@ setupDependencyInjection()
|
||||
dependencies {
|
||||
api(projects.features.location.api)
|
||||
implementation(projects.features.messages.api)
|
||||
implementation(projects.libraries.maplibreCompose)
|
||||
implementation(libs.maplibre.compose)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.di)
|
||||
|
||||
@@ -9,21 +9,30 @@
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import android.Manifest
|
||||
import android.view.Gravity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.maplibre.compose.MapLocationSettings
|
||||
import io.element.android.libraries.maplibre.compose.MapSymbolManagerSettings
|
||||
import io.element.android.libraries.maplibre.compose.MapUiSettings
|
||||
import org.maplibre.android.camera.CameraPosition
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
import androidx.compose.ui.Alignment
|
||||
import org.maplibre.compose.map.GestureOptions
|
||||
import org.maplibre.compose.map.MapOptions
|
||||
import org.maplibre.compose.map.OrnamentOptions
|
||||
import org.maplibre.compose.map.RenderOptions
|
||||
|
||||
/**
|
||||
* Common configuration values for the map.
|
||||
*/
|
||||
object MapDefaults {
|
||||
val options = MapOptions(
|
||||
renderOptions = RenderOptions.Standard,
|
||||
gestureOptions = GestureOptions.Standard,
|
||||
ornamentOptions = OrnamentOptions(
|
||||
isLogoEnabled = true,
|
||||
logoAlignment = Alignment.BottomStart,
|
||||
isAttributionEnabled = true,
|
||||
attributionAlignment = Alignment.BottomEnd,
|
||||
isCompassEnabled = false,
|
||||
isScaleBarEnabled = false,
|
||||
)
|
||||
)
|
||||
|
||||
/*
|
||||
val uiSettings: MapUiSettings
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
@@ -60,6 +69,8 @@ object MapDefaults {
|
||||
.zoom(2.7)
|
||||
.build()
|
||||
|
||||
*/
|
||||
|
||||
const val DEFAULT_ZOOM = 15.0
|
||||
|
||||
val permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
package io.element.android.features.location.impl.common.ui
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -30,13 +32,11 @@ internal fun LocationFloatingActionButton(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
FloatingActionButton(
|
||||
shape = FloatingActionButtonDefaults.smallShape,
|
||||
shape = CircleShape,
|
||||
containerColor = ElementTheme.colors.bgCanvasDefault,
|
||||
contentColor = ElementTheme.colors.iconPrimary,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
// Note: design is 40dp, but min is 48 for accessibility.
|
||||
.size(48.dp),
|
||||
modifier = modifier.size(48.dp),
|
||||
) {
|
||||
val iconImage = if (isMapCenteredOnUser) {
|
||||
CompoundIcons.LocationNavigatorCentred()
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (c) 2025 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.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||
import androidx.compose.material3.BottomSheetDefaults
|
||||
import androidx.compose.material3.BottomSheetScaffoldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.max
|
||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||
import io.element.android.features.location.impl.common.MapDefaults
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
||||
import kotlin.math.roundToInt
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.map.MapOptions
|
||||
import org.maplibre.compose.map.MaplibreMap
|
||||
import org.maplibre.compose.style.BaseStyle
|
||||
import org.maplibre.compose.util.MaplibreComposable
|
||||
|
||||
/**
|
||||
* A reusable scaffold component for map views with a bottom sheet.
|
||||
*
|
||||
* Handles the layout complexity of:
|
||||
* - Calculating the visible sheet height dynamically
|
||||
* - Updating camera position padding based on sheet height
|
||||
* - Rendering the MaplibreMap with proper ornament positioning
|
||||
*
|
||||
* @param cameraState The camera state for the map
|
||||
* @param topBar The top app bar content
|
||||
* @param sheetContent The content to display in the bottom sheet
|
||||
* @param modifier Modifier for the root layout
|
||||
* @param scaffoldState State for the bottom sheet scaffold
|
||||
* @param sheetPeekHeight The height of the sheet when collapsed
|
||||
* @param sheetDragHandle Optional drag handle for the sheet
|
||||
* @param sheetSwipeEnabled Whether the sheet can be swiped
|
||||
* @param snackbarHost The snackbar host content
|
||||
* @param mapContent The content inside the MaplibreMap (layers, location pucks, etc.)
|
||||
* @param overlayContent Content to overlay on top of the map (FAB, pin icons, etc.)
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MapBottomSheetScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.PartiallyExpanded)
|
||||
),
|
||||
cameraState: CameraState = rememberCameraState(),
|
||||
mapOptions: MapOptions = MapDefaults.options,
|
||||
sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight,
|
||||
sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
sheetSwipeEnabled: Boolean = true,
|
||||
topBar: (@Composable () -> Unit)? = null,
|
||||
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
|
||||
sheetContent: @Composable ColumnScope.() -> Unit = {},
|
||||
mapContent: @Composable @MaplibreComposable () -> Unit = {},
|
||||
overlayContent: @Composable BoxScope.(sheetPadding: PaddingValues) -> Unit = {},
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
BoxWithConstraints(modifier = modifier.safeDrawingPadding()) {
|
||||
val layoutHeightPx by rememberUpdatedState(constraints.maxHeight)
|
||||
val sheetPadding by remember {
|
||||
derivedStateOf {
|
||||
val sheetOffset = tryOrNull { scaffoldState.bottomSheetState.requireOffset() } ?: 0f
|
||||
val sheetVisibleHeightPx = layoutHeightPx - sheetOffset
|
||||
val bottomPadding = with(density) { max(sheetVisibleHeightPx.roundToInt().toDp(), 0.dp) }
|
||||
PaddingValues(bottom = bottomPadding)
|
||||
}
|
||||
}
|
||||
// Update camera position when sheet padding changes
|
||||
LaunchedEffect(sheetPadding) {
|
||||
cameraState.position = cameraState.position.copy(padding = sheetPadding)
|
||||
}
|
||||
|
||||
BottomSheetScaffold(
|
||||
sheetPeekHeight = sheetPeekHeight,
|
||||
sheetContent = {
|
||||
sheetContent()
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
},
|
||||
scaffoldState = scaffoldState,
|
||||
sheetDragHandle = sheetDragHandle,
|
||||
sheetSwipeEnabled = sheetSwipeEnabled,
|
||||
snackbarHost = snackbarHost,
|
||||
topBar = topBar,
|
||||
) {
|
||||
val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding)
|
||||
Box {
|
||||
MaplibreMap(
|
||||
options = mapOptions.copy(ornamentOptions = ornamentOptions),
|
||||
baseStyle = BaseStyle.Uri(rememberTileStyleUrl()),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
cameraState = cameraState,
|
||||
content = mapContent,
|
||||
)
|
||||
overlayContent(sheetPadding)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.location.LocationPuck
|
||||
import org.maplibre.compose.location.LocationPuckColors
|
||||
import org.maplibre.compose.location.LocationPuckSizes
|
||||
import org.maplibre.compose.location.LocationTrackingEffect
|
||||
import org.maplibre.compose.location.UserLocationState
|
||||
|
||||
@Composable
|
||||
fun UserLocation(
|
||||
cameraState: CameraState,
|
||||
locationState: UserLocationState,
|
||||
trackUserLocation: Boolean,
|
||||
) {
|
||||
LocationTrackingEffect(
|
||||
locationState = locationState,
|
||||
enabled = trackUserLocation,
|
||||
) {
|
||||
cameraState.updateFromLocation()
|
||||
}
|
||||
val location = locationState.location
|
||||
if (location != null) {
|
||||
LocationPuck(
|
||||
idPrefix = "user-location",
|
||||
locationState = locationState,
|
||||
cameraState = cameraState,
|
||||
accuracyThreshold = Float.POSITIVE_INFINITY,
|
||||
showBearingAccuracy = false,
|
||||
showBearing = false,
|
||||
sizes = LocationPuckSizes(
|
||||
dotRadius = 8.dp,
|
||||
dotStrokeWidth = 2.dp,
|
||||
),
|
||||
colors = LocationPuckColors(
|
||||
dotFillColorCurrentLocation = ElementTheme.colors.iconAccentPrimary,
|
||||
dotFillColorOldLocation = ElementTheme.colors.iconAccentTertiary,
|
||||
dotStrokeColor = ElementTheme.colors.bgCanvasDefault,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,21 +13,15 @@ import kotlin.time.Duration
|
||||
|
||||
sealed interface ShareLocationEvent {
|
||||
data class ShareStaticLocation(
|
||||
val cameraPosition: CameraPosition,
|
||||
val location: Location?,
|
||||
) : ShareLocationEvent {
|
||||
data class CameraPosition(
|
||||
val lat: Double,
|
||||
val lon: Double,
|
||||
val zoom: Double,
|
||||
)
|
||||
}
|
||||
val location: Location,
|
||||
val isPinned: Boolean,
|
||||
) : ShareLocationEvent
|
||||
|
||||
data object SelectLiveLocationDuration : ShareLocationEvent
|
||||
data object ShowLiveLocationDurationPicker : ShareLocationEvent
|
||||
data class StartLiveLocationShare(val duration: Duration) : ShareLocationEvent
|
||||
|
||||
data object SwitchToMyLocationMode : ShareLocationEvent
|
||||
data object SwitchToPinLocationMode : ShareLocationEvent
|
||||
data object StartTrackingUserPosition : ShareLocationEvent
|
||||
data object StopTrackingUserPosition : ShareLocationEvent
|
||||
data object DismissDialog : ShareLocationEvent
|
||||
data object RequestPermissions : ShareLocationEvent
|
||||
data object OpenAppSettings : ShareLocationEvent
|
||||
|
||||
@@ -61,15 +61,7 @@ class ShareLocationPresenter(
|
||||
@Composable
|
||||
override fun present(): ShareLocationState {
|
||||
val permissionsState: PermissionsState = permissionsPresenter.present()
|
||||
var mode: ShareLocationState.Mode by remember {
|
||||
mutableStateOf(
|
||||
if (permissionsState.isAnyGranted) {
|
||||
ShareLocationState.Mode.SenderLocation
|
||||
} else {
|
||||
ShareLocationState.Mode.PinLocation
|
||||
}
|
||||
)
|
||||
}
|
||||
var trackUserPosition: Boolean by remember { mutableStateOf(permissionsState.isAnyGranted) }
|
||||
val isLiveLocationSharingEnabled by remember {
|
||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing)
|
||||
}.collectAsState(false)
|
||||
@@ -81,7 +73,7 @@ class ShareLocationPresenter(
|
||||
|
||||
LaunchedEffect(permissionsState.permissions) {
|
||||
if (permissionsState.isAnyGranted) {
|
||||
mode = ShareLocationState.Mode.SenderLocation
|
||||
trackUserPosition = true
|
||||
dialogState = ShareLocationState.Dialog.None
|
||||
}
|
||||
}
|
||||
@@ -89,21 +81,21 @@ class ShareLocationPresenter(
|
||||
fun handleEvent(event: ShareLocationEvent) {
|
||||
when (event) {
|
||||
is ShareLocationEvent.ShareStaticLocation -> scope.launch {
|
||||
shareLocation(event, mode)
|
||||
shareStaticLocation(event)
|
||||
}
|
||||
ShareLocationEvent.SwitchToMyLocationMode -> when {
|
||||
permissionsState.isAnyGranted -> mode = ShareLocationState.Mode.SenderLocation
|
||||
ShareLocationEvent.StartTrackingUserPosition -> when {
|
||||
permissionsState.isAnyGranted -> trackUserPosition = true
|
||||
permissionsState.shouldShowRationale -> dialogState = ShareLocationState.Dialog.PermissionRationale
|
||||
else -> dialogState = ShareLocationState.Dialog.PermissionDenied
|
||||
}
|
||||
ShareLocationEvent.SwitchToPinLocationMode -> mode = ShareLocationState.Mode.PinLocation
|
||||
ShareLocationEvent.StopTrackingUserPosition -> trackUserPosition = false
|
||||
ShareLocationEvent.DismissDialog -> dialogState = ShareLocationState.Dialog.None
|
||||
ShareLocationEvent.OpenAppSettings -> {
|
||||
locationActions.openSettings()
|
||||
dialogState = ShareLocationState.Dialog.None
|
||||
}
|
||||
ShareLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
|
||||
ShareLocationEvent.SelectLiveLocationDuration -> dialogState = when {
|
||||
ShareLocationEvent.ShowLiveLocationDurationPicker -> dialogState = when {
|
||||
permissionsState.isAnyGranted -> ShareLocationState.Dialog.LiveLocationDuration
|
||||
permissionsState.shouldShowRationale -> ShareLocationState.Dialog.PermissionRationale
|
||||
else -> ShareLocationState.Dialog.PermissionDenied
|
||||
@@ -117,7 +109,7 @@ class ShareLocationPresenter(
|
||||
|
||||
return ShareLocationState(
|
||||
dialogState = dialogState,
|
||||
mode = mode,
|
||||
trackUserLocation = trackUserPosition,
|
||||
hasLocationPermission = permissionsState.isAnyGranted,
|
||||
canShareLiveLocation = isLiveLocationSharingEnabled,
|
||||
appName = appName,
|
||||
@@ -125,56 +117,28 @@ class ShareLocationPresenter(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun shareLocation(
|
||||
event: ShareLocationEvent.ShareStaticLocation,
|
||||
mode: ShareLocationState.Mode,
|
||||
) {
|
||||
private suspend fun shareStaticLocation(event: ShareLocationEvent.ShareStaticLocation) {
|
||||
val replyMode = messageComposerContext.composerMode as? MessageComposerMode.Reply
|
||||
val inReplyToEventId = replyMode?.eventId
|
||||
when (mode) {
|
||||
ShareLocationState.Mode.PinLocation -> {
|
||||
val geoUri = event.cameraPosition.toGeoUri()
|
||||
getTimeline().flatMap {
|
||||
it.sendLocation(
|
||||
body = generateBody(geoUri),
|
||||
geoUri = geoUri,
|
||||
description = null,
|
||||
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
|
||||
assetType = AssetType.PIN,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
analyticsService.capture(
|
||||
Composer(
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
messageType = Composer.MessageType.LocationPin,
|
||||
)
|
||||
)
|
||||
}
|
||||
ShareLocationState.Mode.SenderLocation -> {
|
||||
val geoUri = event.toGeoUri()
|
||||
getTimeline().flatMap {
|
||||
it.sendLocation(
|
||||
body = generateBody(geoUri),
|
||||
geoUri = geoUri,
|
||||
description = null,
|
||||
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
|
||||
assetType = AssetType.SENDER,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
analyticsService.capture(
|
||||
Composer(
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
messageType = Composer.MessageType.LocationUser,
|
||||
)
|
||||
)
|
||||
}
|
||||
val geoUri = event.location.toGeoUri()
|
||||
getTimeline().flatMap {
|
||||
it.sendLocation(
|
||||
body = generateBody(geoUri),
|
||||
geoUri = geoUri,
|
||||
description = null,
|
||||
zoomLevel = MapDefaults.DEFAULT_ZOOM.toInt(),
|
||||
assetType = if (event.isPinned) AssetType.PIN else AssetType.SENDER,
|
||||
inReplyToEventId = inReplyToEventId,
|
||||
)
|
||||
}
|
||||
analyticsService.capture(
|
||||
Composer(
|
||||
inThread = messageComposerContext.composerMode.inThread,
|
||||
isEditing = messageComposerContext.composerMode.isEditing,
|
||||
isReply = messageComposerContext.composerMode.isReply,
|
||||
messageType = if (event.isPinned) Composer.MessageType.LocationPin else Composer.MessageType.LocationUser
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getTimeline(): Result<Timeline> {
|
||||
@@ -185,8 +149,4 @@ class ShareLocationPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun ShareLocationEvent.ShareStaticLocation.toGeoUri(): String = location?.toGeoUri() ?: cameraPosition.toGeoUri()
|
||||
|
||||
private fun ShareLocationEvent.ShareStaticLocation.CameraPosition.toGeoUri(): String = "geo:$lat,$lon"
|
||||
|
||||
private fun generateBody(uri: String): String = "Location was shared at $uri"
|
||||
|
||||
@@ -10,17 +10,12 @@ package io.element.android.features.location.impl.share
|
||||
|
||||
data class ShareLocationState(
|
||||
val dialogState: Dialog,
|
||||
val mode: Mode,
|
||||
val trackUserLocation: Boolean,
|
||||
val hasLocationPermission: Boolean,
|
||||
val appName: String,
|
||||
val canShareLiveLocation: Boolean,
|
||||
val eventSink: (ShareLocationEvent) -> Unit,
|
||||
) {
|
||||
sealed interface Mode {
|
||||
data object SenderLocation : Mode
|
||||
data object PinLocation : Mode
|
||||
}
|
||||
|
||||
sealed interface Dialog {
|
||||
data object None : Dialog
|
||||
data object PermissionRationale : Dialog
|
||||
|
||||
@@ -17,32 +17,32 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
||||
get() = sequenceOf(
|
||||
aShareLocationState(
|
||||
permissionDialog = ShareLocationState.Dialog.None,
|
||||
mode = ShareLocationState.Mode.PinLocation,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
permissionDialog = ShareLocationState.Dialog.PermissionDenied,
|
||||
mode = ShareLocationState.Mode.PinLocation,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
permissionDialog = ShareLocationState.Dialog.PermissionRationale,
|
||||
mode = ShareLocationState.Mode.PinLocation,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = false,
|
||||
),
|
||||
aShareLocationState(
|
||||
permissionDialog = ShareLocationState.Dialog.None,
|
||||
mode = ShareLocationState.Mode.PinLocation,
|
||||
trackUserPosition = false,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
permissionDialog = ShareLocationState.Dialog.None,
|
||||
mode = ShareLocationState.Mode.SenderLocation,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
),
|
||||
aShareLocationState(
|
||||
permissionDialog = ShareLocationState.Dialog.LiveLocationDuration,
|
||||
mode = ShareLocationState.Mode.SenderLocation,
|
||||
trackUserPosition = true,
|
||||
hasLocationPermission = true,
|
||||
canShareLiveLocation = true,
|
||||
),
|
||||
@@ -51,13 +51,13 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
||||
|
||||
private fun aShareLocationState(
|
||||
permissionDialog: ShareLocationState.Dialog,
|
||||
mode: ShareLocationState.Mode,
|
||||
trackUserPosition: Boolean,
|
||||
hasLocationPermission: Boolean,
|
||||
canShareLiveLocation: Boolean = false,
|
||||
): ShareLocationState {
|
||||
return ShareLocationState(
|
||||
dialogState = permissionDialog,
|
||||
mode = mode,
|
||||
trackUserLocation = trackUserPosition,
|
||||
hasLocationPermission = hasLocationPermission,
|
||||
canShareLiveLocation = canShareLiveLocation,
|
||||
appName = APP_NAME,
|
||||
|
||||
@@ -12,18 +12,14 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberStandardBottomSheetState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -40,32 +36,36 @@ import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.location.api.Location
|
||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||
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.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.UserLocation
|
||||
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.list.ListItemContent
|
||||
import io.element.android.libraries.designsystem.components.list.RadioButtonListItem
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
||||
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.ListItem
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.maplibre.compose.CameraMode
|
||||
import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason
|
||||
import io.element.android.libraries.maplibre.compose.CameraPositionState
|
||||
import io.element.android.libraries.maplibre.compose.MapLibreMap
|
||||
import io.element.android.libraries.maplibre.compose.rememberCameraPositionState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import org.maplibre.android.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraMoveReason
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.camera.CameraState
|
||||
import org.maplibre.compose.camera.rememberCameraState
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.UserLocationState
|
||||
import org.maplibre.compose.location.rememberDefaultLocationProvider
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
import org.maplibre.compose.location.rememberUserLocationState
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -99,76 +99,32 @@ fun ShareLocationView(
|
||||
)
|
||||
}
|
||||
|
||||
val cameraPositionState = rememberCameraPositionState {
|
||||
position = MapDefaults.centerCameraPosition
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
|
||||
)
|
||||
val cameraState = rememberCameraState(firstPosition = CameraPosition(zoom = MapDefaults.DEFAULT_ZOOM))
|
||||
val locationProvider = if (state.hasLocationPermission) {
|
||||
rememberDefaultLocationProvider(
|
||||
updateInterval = 1.minutes,
|
||||
desiredAccuracy = DesiredAccuracy.Balanced,
|
||||
minDistanceMeters = 50.0,
|
||||
)
|
||||
} else {
|
||||
rememberNullLocationProvider()
|
||||
}
|
||||
val userLocationState = rememberUserLocationState(locationProvider)
|
||||
|
||||
LaunchedEffect(state.mode) {
|
||||
when (state.mode) {
|
||||
ShareLocationState.Mode.PinLocation -> {
|
||||
cameraPositionState.cameraMode = CameraMode.NONE
|
||||
}
|
||||
ShareLocationState.Mode.SenderLocation -> {
|
||||
cameraPositionState.position = CameraPosition.Builder()
|
||||
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||
.build()
|
||||
cameraPositionState.cameraMode = CameraMode.TRACKING
|
||||
}
|
||||
LaunchedEffect(cameraState.isCameraMoving) {
|
||||
if (cameraState.moveReason == CameraMoveReason.GESTURE) {
|
||||
state.eventSink(ShareLocationEvent.StopTrackingUserPosition)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraPositionState.isMoving) {
|
||||
if (cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) {
|
||||
state.eventSink(ShareLocationEvent.SwitchToPinLocationMode)
|
||||
}
|
||||
}
|
||||
|
||||
// BottomSheetScaffold doesn't manage the system insets for sheetContent and the FAB, so we need to do it manually.
|
||||
val navBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
BottomSheetScaffold(
|
||||
sheetContent = {
|
||||
Spacer(Modifier.height(20.dp))
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = "Sharing options",
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
)
|
||||
}
|
||||
)
|
||||
StaticLocationItem(state.mode, cameraPositionState){
|
||||
val positionTarget = cameraPositionState.position.target ?: return@StaticLocationItem
|
||||
state.eventSink(
|
||||
ShareLocationEvent.ShareStaticLocation(
|
||||
cameraPosition = ShareLocationEvent.ShareStaticLocation.CameraPosition(
|
||||
lat = positionTarget.latitude,
|
||||
lon = positionTarget.longitude,
|
||||
zoom = cameraPositionState.position.zoom,
|
||||
),
|
||||
location = cameraPositionState.location?.let {
|
||||
Location(
|
||||
lat = it.latitude,
|
||||
lon = it.longitude,
|
||||
accuracy = it.accuracy,
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
navigateUp()
|
||||
}
|
||||
if(state.canShareLiveLocation){
|
||||
LiveLocationItem {
|
||||
state.eventSink(ShareLocationEvent.SelectLiveLocationDuration)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp + navBarPadding))
|
||||
},
|
||||
MapBottomSheetScaffold(
|
||||
cameraState = cameraState,
|
||||
modifier = modifier,
|
||||
scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded),
|
||||
),
|
||||
sheetDragHandle = {},
|
||||
scaffoldState = scaffoldState,
|
||||
sheetDragHandle = null,
|
||||
sheetSwipeEnabled = false,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
@@ -178,62 +134,102 @@ fun ShareLocationView(
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.consumeWindowInsets(it),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MapLibreMap(
|
||||
styleUri = rememberTileStyleUrl(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
cameraPositionState = cameraPositionState,
|
||||
uiSettings = MapDefaults.uiSettings,
|
||||
symbolManagerSettings = MapDefaults.symbolManagerSettings,
|
||||
locationSettings = MapDefaults.locationSettings.copy(
|
||||
locationEnabled = state.hasLocationPermission,
|
||||
),
|
||||
sheetContent = {
|
||||
BottomSheetContent(
|
||||
cameraState = cameraState,
|
||||
state = state,
|
||||
userLocationState = userLocationState,
|
||||
navigateUp = navigateUp
|
||||
)
|
||||
Icon(
|
||||
resourceId = CommonDrawables.pin,
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.centerBottomEdge(this),
|
||||
},
|
||||
mapContent = {
|
||||
UserLocation(
|
||||
cameraState = cameraState,
|
||||
locationState = userLocationState,
|
||||
trackUserLocation = state.trackUserLocation
|
||||
)
|
||||
LocationFloatingActionButton(
|
||||
isMapCenteredOnUser = state.mode == ShareLocationState.Mode.SenderLocation,
|
||||
onClick = { state.eventSink(ShareLocationEvent.SwitchToMyLocationMode) },
|
||||
},
|
||||
overlayContent = { sheetPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 18.dp, bottom = 72.dp + navBarPadding),
|
||||
.fillMaxSize()
|
||||
.padding(sheetPadding)
|
||||
) {
|
||||
Icon(
|
||||
resourceId = CommonDrawables.pin,
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
modifier = Modifier.centerBottomEdge(this),
|
||||
)
|
||||
}
|
||||
LocationFloatingActionButton(
|
||||
isMapCenteredOnUser = state.trackUserLocation,
|
||||
onClick = { state.eventSink(ShareLocationEvent.StartTrackingUserPosition) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(all = 16.dp),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomSheetContent(
|
||||
cameraState: CameraState,
|
||||
state: ShareLocationState,
|
||||
userLocationState: UserLocationState,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
Spacer(Modifier.height(20.dp))
|
||||
SharePinLocationItem(
|
||||
onClick = {
|
||||
val positionTarget = cameraState.position.target
|
||||
state.eventSink(
|
||||
ShareLocationEvent.ShareStaticLocation(
|
||||
location = Location(lat = positionTarget.latitude, lon = positionTarget.longitude),
|
||||
isPinned = true
|
||||
)
|
||||
)
|
||||
navigateUp()
|
||||
}
|
||||
)
|
||||
ShareCurrentLocationItem(
|
||||
onClick = {
|
||||
val userLocation = userLocationState.location
|
||||
if (state.hasLocationPermission) {
|
||||
if (userLocation == null) {
|
||||
//
|
||||
} else {
|
||||
state.eventSink(
|
||||
ShareLocationEvent.ShareStaticLocation(
|
||||
location = Location(
|
||||
lat = userLocation.position.latitude,
|
||||
lon = userLocation.position.longitude
|
||||
),
|
||||
isPinned = false
|
||||
)
|
||||
)
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
if (state.canShareLiveLocation) {
|
||||
ShareLiveLocationItem {
|
||||
state.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StaticLocationItem(
|
||||
mode: ShareLocationState.Mode,
|
||||
cameraPositionState: CameraPositionState,
|
||||
onClick: ()->Unit,
|
||||
private fun ShareCurrentLocationItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(
|
||||
when (mode) {
|
||||
ShareLocationState.Mode.PinLocation -> CommonStrings.screen_share_this_location_action
|
||||
ShareLocationState.Mode.SenderLocation -> CommonStrings.screen_share_my_location_action
|
||||
}
|
||||
)
|
||||
)
|
||||
Text(stringResource(CommonStrings.screen_share_my_location_action))
|
||||
},
|
||||
modifier = Modifier.clickable(
|
||||
// target is null when the map hasn't loaded (or api key is wrong) so we disable the button
|
||||
enabled = cameraPositionState.position.target != null,
|
||||
onClick = onClick
|
||||
),
|
||||
modifier = Modifier.clickable(onClick = onClick),
|
||||
leadingContent = ListItemContent.Icon(
|
||||
iconSource = IconSource.Vector(CompoundIcons.LocationNavigatorCentred())
|
||||
)
|
||||
@@ -241,7 +237,22 @@ private fun StaticLocationItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LiveLocationItem(
|
||||
private fun SharePinLocationItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(stringResource(CommonStrings.screen_share_this_location_action))
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onClick),
|
||||
leadingContent = ListItemContent.Icon(
|
||||
iconSource = IconSource.Vector(CompoundIcons.LocationNavigator())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShareLiveLocationItem(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
|
||||
@@ -8,15 +8,16 @@
|
||||
|
||||
package io.element.android.features.location.impl.show
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -24,31 +25,35 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.compound.tokens.generated.TypographyTokens
|
||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||
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.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.MapBottomSheetScaffold
|
||||
import io.element.android.features.location.impl.common.ui.UserLocation
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
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.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.maplibre.compose.CameraMode
|
||||
import io.element.android.libraries.maplibre.compose.CameraMoveStartedReason
|
||||
import io.element.android.libraries.maplibre.compose.IconAnchor
|
||||
import io.element.android.libraries.maplibre.compose.MapLibreMap
|
||||
import io.element.android.libraries.maplibre.compose.Symbol
|
||||
import io.element.android.libraries.maplibre.compose.rememberCameraPositionState
|
||||
import io.element.android.libraries.maplibre.compose.rememberSymbolState
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import org.maplibre.android.camera.CameraPosition
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
import org.maplibre.compose.camera.CameraMoveReason
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
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.rememberDefaultLocationProvider
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
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 kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -71,33 +76,32 @@ fun ShowLocationView(
|
||||
)
|
||||
}
|
||||
|
||||
val cameraPositionState = rememberCameraPositionState {
|
||||
position = CameraPosition.Builder()
|
||||
.target(LatLng(state.location.lat, state.location.lon))
|
||||
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||
.build()
|
||||
val cameraState = rememberCameraState(
|
||||
firstPosition = CameraPosition(
|
||||
target = Position(latitude = state.location.lat, longitude = state.location.lon),
|
||||
zoom = MapDefaults.DEFAULT_ZOOM
|
||||
)
|
||||
)
|
||||
val locationProvider = if (state.hasLocationPermission) {
|
||||
rememberDefaultLocationProvider(
|
||||
updateInterval = 1.minutes,
|
||||
desiredAccuracy = DesiredAccuracy.Balanced,
|
||||
minDistanceMeters = 50.0,
|
||||
)
|
||||
} else {
|
||||
rememberNullLocationProvider()
|
||||
}
|
||||
|
||||
LaunchedEffect(state.isTrackMyLocation) {
|
||||
when (state.isTrackMyLocation) {
|
||||
false -> cameraPositionState.cameraMode = CameraMode.NONE
|
||||
true -> {
|
||||
cameraPositionState.position = CameraPosition.Builder()
|
||||
.zoom(MapDefaults.DEFAULT_ZOOM)
|
||||
.build()
|
||||
cameraPositionState.cameraMode = CameraMode.TRACKING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(cameraPositionState.isMoving) {
|
||||
if (cameraPositionState.cameraMoveStartedReason == CameraMoveStartedReason.GESTURE) {
|
||||
val userLocationState = rememberUserLocationState(locationProvider)
|
||||
LaunchedEffect(cameraState.isCameraMoving) {
|
||||
if (cameraState.moveReason == CameraMoveReason.GESTURE) {
|
||||
state.eventSink(ShowLocationEvents.TrackMyLocation(false))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
MapBottomSheetScaffold(
|
||||
cameraState = cameraState,
|
||||
modifier = modifier,
|
||||
sheetPeekHeight = 80.dp,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
titleStr = stringResource(CommonStrings.screen_view_location_title),
|
||||
@@ -118,19 +122,7 @@ fun ShowLocationView(
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
LocationFloatingActionButton(
|
||||
isMapCenteredOnUser = state.isTrackMyLocation,
|
||||
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.consumeWindowInsets(paddingValues)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
sheetContent = {
|
||||
state.description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
@@ -143,28 +135,40 @@ fun ShowLocationView(
|
||||
.padding(8.dp),
|
||||
)
|
||||
}
|
||||
|
||||
MapLibreMap(
|
||||
styleUri = rememberTileStyleUrl(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
images = mapOf(PIN_ID to CommonDrawables.pin).toImmutableMap(),
|
||||
cameraPositionState = cameraPositionState,
|
||||
uiSettings = MapDefaults.uiSettings,
|
||||
symbolManagerSettings = MapDefaults.symbolManagerSettings,
|
||||
locationSettings = MapDefaults.locationSettings.copy(
|
||||
locationEnabled = state.hasLocationPermission,
|
||||
),
|
||||
) {
|
||||
Symbol(
|
||||
iconId = PIN_ID,
|
||||
state = rememberSymbolState(
|
||||
position = LatLng(state.location.lat, state.location.lon)
|
||||
),
|
||||
iconAnchor = IconAnchor.BOTTOM,
|
||||
},
|
||||
mapContent = {
|
||||
UserLocation(
|
||||
cameraState = cameraState,
|
||||
locationState = userLocationState,
|
||||
trackUserLocation = state.isTrackMyLocation
|
||||
)
|
||||
val senderLocation = rememberGeoJsonSource(
|
||||
data = GeoJsonData.Features(
|
||||
Point(
|
||||
Position(
|
||||
latitude = state.location.lat,
|
||||
longitude = state.location.lon
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
val marker = painterResource(R.drawable.pin_small)
|
||||
SymbolLayer(
|
||||
id = "sender_location",
|
||||
source = senderLocation,
|
||||
iconImage = image(marker)
|
||||
)
|
||||
},
|
||||
overlayContent = {
|
||||
LocationFloatingActionButton(
|
||||
isMapCenteredOnUser = state.isTrackMyLocation,
|
||||
onClick = { state.eventSink(ShowLocationEvents.TrackMyLocation(true)) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(all = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@@ -175,5 +179,3 @@ internal fun ShowLocationViewPreview(@PreviewParameter(ShowLocationStateProvider
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
private const val PIN_ID = "pin"
|
||||
|
||||
Reference in New Issue
Block a user