diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/NavigationViewComponentBuilder.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/NavigationViewComponentBuilder.kt new file mode 100644 index 00000000..d3fd8568 --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/NavigationViewComponentBuilder.kt @@ -0,0 +1,103 @@ +package com.stadiamaps.ferrostar.composeui.config + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.stadiamaps.ferrostar.composeui.models.CameraControlState +import com.stadiamaps.ferrostar.composeui.theme.DefaultNavigationUITheme +import com.stadiamaps.ferrostar.composeui.theme.NavigationUITheme +import com.stadiamaps.ferrostar.composeui.views.components.CurrentRoadNameView +import com.stadiamaps.ferrostar.composeui.views.components.InstructionsView +import com.stadiamaps.ferrostar.composeui.views.components.TripProgressView +import com.stadiamaps.ferrostar.core.NavigationUiState + +data class NavigationViewComponentBuilder( + val instructionsView: @Composable (modifier: Modifier, uiState: NavigationUiState) -> Unit, + val progressView: + @Composable + (modifier: Modifier, uiState: NavigationUiState, onTapExit: (() -> Unit)?) -> Unit, + val streetNameView: + @Composable + (modifier: Modifier, roadName: String?, cameraControlState: CameraControlState) -> Unit, + val customOverlayView: @Composable (BoxScope.(Modifier) -> Unit)? = null, + // TODO: We may reasonably be able to add the NavigationMapView here. But not sure how much + // value that would add + // since most of the hard config already exists within the overlay views which are not + // maplibre specific. +) { + companion object { + fun Default(theme: NavigationUITheme = DefaultNavigationUITheme) = + NavigationViewComponentBuilder( + instructionsView = { modifier, uiState -> + uiState.visualInstruction?.let { instructions -> + InstructionsView( + modifier = modifier, + instructions = instructions, + theme = theme.instructionRowTheme, + remainingSteps = uiState.remainingSteps, + distanceToNextManeuver = uiState.progress?.distanceToNextManeuver) + } + }, + progressView = { modifier, uiState, onTapExit -> + uiState.progress?.let { progress -> + TripProgressView( + modifier = modifier, + theme = theme.tripProgressViewTheme, + progress = progress, + onTapExit = onTapExit) + } + }, + streetNameView = { modifier, roadName, cameraControlState -> + if (cameraControlState is CameraControlState.ShowRouteOverview) { + roadName?.let { roadName -> + Row( + modifier.fillMaxWidth(), + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.Center) { + CurrentRoadNameView( + modifier = modifier, + theme = theme.roadNameViewTheme, + currentRoadName = roadName) + + Spacer(modifier = Modifier.height(8.dp)) + } + } + } + }) + } +} + +fun NavigationViewComponentBuilder.withInstructionsView( + instructionsView: @Composable (modifier: Modifier, uiState: NavigationUiState) -> Unit +): NavigationViewComponentBuilder { + return copy(instructionsView = instructionsView) +} + +fun NavigationViewComponentBuilder.withProgressView( + progressView: + @Composable + (modifier: Modifier, uiState: NavigationUiState, onTapExit: (() -> Unit)?) -> Unit +): NavigationViewComponentBuilder { + return copy(progressView = progressView) +} + +fun NavigationViewComponentBuilder.withStreetNameView( + streetNameView: + @Composable + (modifier: Modifier, roadName: String?, cameraControlState: CameraControlState) -> Unit +): NavigationViewComponentBuilder { + return copy(streetNameView = streetNameView) +} + +fun NavigationViewComponentBuilder.withCustomOverlayView( + customOverlayView: @Composable (BoxScope.(Modifier) -> Unit) +): NavigationViewComponentBuilder { + return copy(customOverlayView = customOverlayView) +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt index 0ca3fb93..509a3949 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt @@ -1,20 +1,14 @@ package com.stadiamaps.ferrostar.composeui.config -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp - -sealed class CameraControlState { - data object Hidden : CameraControlState() - - data class ShowRecenter(val updateCamera: () -> Unit) : CameraControlState() - - data class ShowRouteOverview(val updateCamera: () -> Unit) : CameraControlState() -} - data class VisualNavigationViewConfig( + // Mute var showMute: Boolean = false, + var onMute: (() -> Unit)? = null, + + // Zoom var showZoom: Boolean = false, - var buttonSize: DpSize = DpSize(56.dp, 56.dp) + var onZoomIn: (() -> Unit)? = null, + var onZoomOut: (() -> Unit)? = null, ) { companion object { fun Default() = VisualNavigationViewConfig(showMute = true, showZoom = true) @@ -22,19 +16,14 @@ data class VisualNavigationViewConfig( } /** Enables the mute button in the navigation view. */ -fun VisualNavigationViewConfig.useMuteButton(): VisualNavigationViewConfig { - showMute = true - return this +fun VisualNavigationViewConfig.useMuteButton(onMute: () -> Unit): VisualNavigationViewConfig { + return copy(showMute = true, onMute = onMute) } /** Enables the zoom button in the navigation view. */ -fun VisualNavigationViewConfig.useZoomButton(): VisualNavigationViewConfig { - showZoom = true - return this -} - -/** Changes the size of navigation buttons. */ -fun VisualNavigationViewConfig.buttonSize(size: DpSize): VisualNavigationViewConfig { - buttonSize = size - return this +fun VisualNavigationViewConfig.useZoomButton( + onZoomIn: () -> Unit, + onZoomOut: () -> Unit +): VisualNavigationViewConfig { + return copy(showZoom = true, onZoomIn = onZoomIn, onZoomOut = onZoomOut) } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/CameraControlState.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/CameraControlState.kt new file mode 100644 index 00000000..93f293c6 --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/CameraControlState.kt @@ -0,0 +1,9 @@ +package com.stadiamaps.ferrostar.composeui.models + +sealed class CameraControlState { + data object Hidden : CameraControlState() + + data class ShowRecenter(val updateCamera: () -> Unit) : CameraControlState() + + data class ShowRouteOverview(val updateCamera: () -> Unit) : CameraControlState() +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/NavigationViewMetrics.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/NavigationViewMetrics.kt new file mode 100644 index 00000000..55558312 --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/NavigationViewMetrics.kt @@ -0,0 +1,36 @@ +package com.stadiamaps.ferrostar.composeui.models + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +data class NavigationViewMetrics( + val progressViewSize: DpSize = DpSize(0.dp, 0.dp), + val instructionsViewSize: DpSize = DpSize(0.dp, 0.dp), + val buttonSize: DpSize, +) { + + /** + * Returns the MapView's safe insets. + * + * @param start Optional additional start padding. Default is 0.dp. + * @param top Optional additional top padding. Default is instructionsViewSize.height. + * @param end Optional additional end padding. Default is 0.dp. + * @param bottom Optional additional bottom padding. Default is progressViewSize.height. + * @return The calculated padding insets. + */ + fun mapViewInsets( + start: Dp = 0.dp, + top: Dp = 0.dp, + end: Dp = 0.dp, + bottom: Dp = 0.dp, + ): PaddingValues { + return PaddingValues( + start = start, + top = instructionsViewSize.height + top, + end = end, + bottom = progressViewSize.height + buttonSize.height + bottom, + ) + } +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/notification/DefaultForegroundNotificationBuilder.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/notification/DefaultForegroundNotificationBuilder.kt index b4543225..b285a3a3 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/notification/DefaultForegroundNotificationBuilder.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/notification/DefaultForegroundNotificationBuilder.kt @@ -12,7 +12,7 @@ import com.stadiamaps.ferrostar.composeui.formatting.DurationFormatter import com.stadiamaps.ferrostar.composeui.formatting.EstimatedArrivalDateTimeFormatter import com.stadiamaps.ferrostar.composeui.formatting.LocalizedDistanceFormatter import com.stadiamaps.ferrostar.composeui.formatting.LocalizedDurationFormatter -import com.stadiamaps.ferrostar.composeui.views.maneuver.maneuverIcon +import com.stadiamaps.ferrostar.composeui.views.components.maneuver.maneuverIcon import com.stadiamaps.ferrostar.core.extensions.estimatedArrivalTime import com.stadiamaps.ferrostar.core.service.ForegroundNotificationBuilder import uniffi.ferrostar.TripProgress diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/InstructionRowTheme.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/InstructionRowTheme.kt index 43d0ca1d..c9b04263 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/InstructionRowTheme.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/InstructionRowTheme.kt @@ -33,5 +33,5 @@ object DefaultInstructionRowTheme : InstructionRowTheme { @Composable get() = MaterialTheme.colorScheme.onSurface override val backgroundColor: Color - @Composable get() = MaterialTheme.colorScheme.surfaceContainerLow + @Composable get() = MaterialTheme.colorScheme.surface } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/NavigationUITheme.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/NavigationUITheme.kt new file mode 100644 index 00000000..c82bafe4 --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/NavigationUITheme.kt @@ -0,0 +1,26 @@ +package com.stadiamaps.ferrostar.composeui.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp + +interface NavigationUITheme { + @get:Composable val instructionRowTheme: InstructionRowTheme + @get:Composable val roadNameViewTheme: RoadNameViewTheme + @get:Composable val tripProgressViewTheme: TripProgressViewTheme + @get:Composable val buttonSize: DpSize +} + +object DefaultNavigationUITheme : NavigationUITheme { + override val instructionRowTheme: InstructionRowTheme + @Composable get() = DefaultInstructionRowTheme + + override val roadNameViewTheme: RoadNameViewTheme + @Composable get() = DefaultRoadNameViewTheme + + override val tripProgressViewTheme: TripProgressViewTheme + @Composable get() = DefaultTripProgressViewTheme + + override val buttonSize: DpSize + @Composable get() = DpSize(56.dp, 56.dp) +} diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/CurrentRoadView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/CurrentRoadView.kt similarity index 97% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/CurrentRoadView.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/CurrentRoadView.kt index 0256382e..cbce520e 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/CurrentRoadView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/CurrentRoadView.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views +package com.stadiamaps.ferrostar.composeui.views.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/InstructionsView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/InstructionsView.kt similarity index 83% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/InstructionsView.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/InstructionsView.kt index 292574eb..49895aab 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/InstructionsView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/InstructionsView.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views +package com.stadiamaps.ferrostar.composeui.views.components import android.icu.util.ULocale import androidx.compose.animation.animateContentSize @@ -20,7 +20,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -36,9 +35,9 @@ import com.stadiamaps.ferrostar.composeui.formatting.DistanceFormatter import com.stadiamaps.ferrostar.composeui.formatting.LocalizedDistanceFormatter import com.stadiamaps.ferrostar.composeui.theme.DefaultInstructionRowTheme import com.stadiamaps.ferrostar.composeui.theme.InstructionRowTheme -import com.stadiamaps.ferrostar.composeui.views.controls.PillDragHandle -import com.stadiamaps.ferrostar.composeui.views.maneuver.ManeuverImage -import com.stadiamaps.ferrostar.composeui.views.maneuver.ManeuverInstructionView +import com.stadiamaps.ferrostar.composeui.views.components.controls.PillDragHandle +import com.stadiamaps.ferrostar.composeui.views.components.maneuver.ManeuverImage +import com.stadiamaps.ferrostar.composeui.views.components.maneuver.ManeuverInstructionView import uniffi.ferrostar.ManeuverModifier import uniffi.ferrostar.ManeuverType import uniffi.ferrostar.RouteStep @@ -62,11 +61,12 @@ fun InstructionsView( remainingSteps: List? = null, initExpanded: Boolean = false, contentBuilder: @Composable (VisualInstruction) -> Unit = { - ManeuverImage(it.primaryContent, tint = MaterialTheme.colorScheme.primary) + ManeuverImage(it.primaryContent, tint = theme.iconTintColor) } ) { var isExpanded by remember { mutableStateOf(initExpanded) } val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val remainingSteps: List = remainingSteps?.drop(1) ?: emptyList() Box( modifier = @@ -89,7 +89,7 @@ fun InstructionsView( // TODO: Secondary content // Expanded content - val showMultipleRows = isExpanded && remainingSteps != null && remainingSteps.count() > 1 + val showMultipleRows = isExpanded && remainingSteps.count() > 1 if (showMultipleRows) { Spacer(modifier = Modifier.height(8.dp)) HorizontalDivider(thickness = 1.dp) @@ -101,19 +101,17 @@ fun InstructionsView( LazyColumn( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp)) { - if (remainingSteps != null) { - items(remainingSteps.drop(1)) { step -> - step.visualInstructions.firstOrNull()?.let { upcomingInstruction -> - ManeuverInstructionView( - text = upcomingInstruction.primaryContent.text, - distanceFormatter = distanceFormatter, - distanceToNextManeuver = step.distance, - theme = theme) { - contentBuilder(upcomingInstruction) - } - Spacer(modifier = Modifier.height(8.dp)) - HorizontalDivider(thickness = 1.dp) - } + items(remainingSteps) { step -> + step.visualInstructions.firstOrNull()?.let { upcomingInstruction -> + ManeuverInstructionView( + text = upcomingInstruction.primaryContent.text, + distanceFormatter = distanceFormatter, + distanceToNextManeuver = step.distance, + theme = theme) { + contentBuilder(upcomingInstruction) + } + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider(thickness = 1.dp) } } } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/TripProgressView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/TripProgressView.kt similarity index 99% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/TripProgressView.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/TripProgressView.kt index 0cd35ba1..8650a25f 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/TripProgressView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/TripProgressView.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views +package com.stadiamaps.ferrostar.composeui.views.components import android.icu.util.ULocale import androidx.compose.foundation.background diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIButton.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/NavigationUIButton.kt similarity index 96% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIButton.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/NavigationUIButton.kt index fcf8efa3..9c61d250 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIButton.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/NavigationUIButton.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views.controls +package com.stadiamaps.ferrostar.composeui.views.components.controls import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIZoomButton.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/NavigationUIZoomButton.kt similarity index 97% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIZoomButton.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/NavigationUIZoomButton.kt index cd3ee0a3..1bd51dcc 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/NavigationUIZoomButton.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/NavigationUIZoomButton.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views.controls +package com.stadiamaps.ferrostar.composeui.views.components.controls import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/PillDragHandle.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/PillDragHandle.kt similarity index 93% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/PillDragHandle.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/PillDragHandle.kt index 1eb7af00..73e111c7 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/controls/PillDragHandle.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/controls/PillDragHandle.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views.controls +package com.stadiamaps.ferrostar.composeui.views.components.controls import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -30,7 +30,7 @@ fun PillDragHandle( toggle: () -> Unit = {} ) { val handleHeight = if (isExpanded) 36.dp else 4.dp - Box(modifier = modifier.height(handleHeight).clickable(onClick = toggle)) { + Box(modifier = modifier.fillMaxWidth().height(handleHeight).clickable(onClick = toggle)) { if (isExpanded) { Icon( Icons.Rounded.KeyboardArrowUp, diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/InnerGridView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/InnerGridView.kt similarity index 98% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/InnerGridView.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/InnerGridView.kt index 71387d70..12fe9d0f 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/InnerGridView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/InnerGridView.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views.gridviews +package com.stadiamaps.ferrostar.composeui.views.components.gridviews import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/NavigatingInnerGridView.kt similarity index 92% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/NavigatingInnerGridView.kt index a7acfbe0..4554100e 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/gridviews/NavigatingInnerGridView.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views.gridviews +package com.stadiamaps.ferrostar.composeui.views.components.gridviews import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -20,9 +20,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.stadiamaps.ferrostar.composeui.R -import com.stadiamaps.ferrostar.composeui.config.CameraControlState -import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIButton -import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIZoomButton +import com.stadiamaps.ferrostar.composeui.models.CameraControlState +import com.stadiamaps.ferrostar.composeui.views.components.controls.NavigationUIButton +import com.stadiamaps.ferrostar.composeui.views.components.controls.NavigationUIZoomButton @Composable fun NavigatingInnerGridView( @@ -37,6 +37,7 @@ fun NavigatingInnerGridView( onClickZoomOut: () -> Unit = {}, topCenter: @Composable () -> Unit = { Spacer(Modifier.width(12.dp)) }, centerStart: @Composable () -> Unit = { Spacer(Modifier.width(12.dp)) }, + bottomCenter: @Composable () -> Unit = { Spacer(Modifier.width(12.dp)) }, bottomEnd: @Composable () -> Unit = { Spacer(Modifier.width(12.dp)) } ) { InnerGridView( @@ -98,6 +99,7 @@ fun NavigatingInnerGridView( } } }, + bottomCenter = bottomCenter, bottomEnd = bottomEnd) } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverImage.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/maneuver/ManeuverImage.kt similarity index 97% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverImage.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/maneuver/ManeuverImage.kt index 467e1529..a8e21a7c 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverImage.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/maneuver/ManeuverImage.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views.maneuver +package com.stadiamaps.ferrostar.composeui.views.components.maneuver import android.annotation.SuppressLint import androidx.compose.foundation.layout.size diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverInstructionView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/maneuver/ManeuverInstructionView.kt similarity index 97% rename from android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverInstructionView.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/maneuver/ManeuverInstructionView.kt index 40bc7236..9a9a7eac 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverInstructionView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/components/maneuver/ManeuverInstructionView.kt @@ -1,4 +1,4 @@ -package com.stadiamaps.ferrostar.composeui.views.maneuver +package com.stadiamaps.ferrostar.composeui.views.components.maneuver import android.icu.util.ULocale import androidx.compose.foundation.Image diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/LandscapeNavigationOverlayView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/LandscapeNavigationOverlayView.kt similarity index 51% rename from android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/LandscapeNavigationOverlayView.kt rename to android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/LandscapeNavigationOverlayView.kt index a38fcb58..787cf3c3 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/LandscapeNavigationOverlayView.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/LandscapeNavigationOverlayView.kt @@ -1,12 +1,15 @@ -package com.stadiamaps.ferrostar.maplibreui.views.overlays +package com.stadiamaps.ferrostar.composeui.views.overlays import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -17,72 +20,69 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import com.maplibre.compose.camera.MapViewCamera -import com.maplibre.compose.camera.extensions.incrementZoom -import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.NavigationViewComponentBuilder import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig -import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView -import com.stadiamaps.ferrostar.composeui.views.InstructionsView -import com.stadiamaps.ferrostar.composeui.views.TripProgressView -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridView +import com.stadiamaps.ferrostar.composeui.models.CameraControlState +import com.stadiamaps.ferrostar.composeui.models.NavigationViewMetrics +import com.stadiamaps.ferrostar.composeui.theme.DefaultNavigationUITheme +import com.stadiamaps.ferrostar.composeui.theme.NavigationUITheme +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.NavigatingInnerGridView import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel import com.stadiamaps.ferrostar.core.mock.MockNavigationViewModel import com.stadiamaps.ferrostar.core.mock.pedestrianExample -import com.stadiamaps.ferrostar.maplibreui.NavigationViewMetrics -import com.stadiamaps.ferrostar.maplibreui.extensions.cameraControlState -import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @Composable fun LandscapeNavigationOverlayView( modifier: Modifier, - camera: MutableState, - navigationCamera: MapViewCamera, viewModel: NavigationViewModel, + cameraControlState: CameraControlState, + theme: NavigationUITheme = DefaultNavigationUITheme, config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), - progressViewSize: MutableState = remember { mutableStateOf(DpSize.Zero) }, - currentRoadNameView: @Composable (String?) -> Unit = { roadName -> - if (roadName != null) { - CurrentRoadNameView(roadName) - Spacer(modifier = Modifier.height(8.dp)) - } - }, + views: NavigationViewComponentBuilder = NavigationViewComponentBuilder.Default(theme), + mapViewInsets: MutableState, onTapExit: (() -> Unit)? = null, ) { val density = LocalDensity.current + val windowInsets = WindowInsets.statusBars.asPaddingValues() + val halfOfScreen: Dp = with(density) { LocalConfiguration.current.screenWidthDp.dp / 2 } + val uiState by viewModel.uiState.collectAsState() + var instructionsViewSize by remember { mutableStateOf(DpSize.Zero) } + var progressViewSize by remember { mutableStateOf(DpSize.Zero) } + + mapViewInsets.value = + NavigationViewMetrics(buttonSize = theme.buttonSize) + .mapViewInsets( + start = halfOfScreen + 16.dp, + top = 16.dp + windowInsets.calculateTopPadding(), + bottom = 16.dp + windowInsets.calculateBottomPadding()) Row(modifier) { Column(modifier = Modifier.fillMaxHeight().fillMaxWidth(0.5f)) { - uiState.visualInstruction?.let { instructions -> - InstructionsView( - instructions, - modifier = - Modifier.onSizeChanged { - instructionsViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } - }, - remainingSteps = uiState.remainingSteps, - distanceToNextManeuver = uiState.progress?.distanceToNextManeuver) - } + views.instructionsView( + Modifier.onSizeChanged { + instructionsViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } + }, + uiState) Spacer(modifier = Modifier.weight(1f)) - uiState.progress?.let { progress -> - TripProgressView( - modifier = - Modifier.onSizeChanged { - progressViewSize.value = density.run { DpSize(it.width.toDp(), it.height.toDp()) } - }, - progress = progress, - onTapExit = onTapExit) - } + views.progressView( + Modifier.onSizeChanged { + progressViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } + }, + uiState, + onTapExit) } Spacer(modifier = Modifier.width(16.dp)) @@ -93,17 +93,14 @@ fun LandscapeNavigationOverlayView( showMute = config.showMute, isMuted = uiState.isMuted, onClickMute = { viewModel.toggleMute() }, - buttonSize = config.buttonSize, - cameraControlState = - config.cameraControlState( - camera, - navigationCamera, - uiState, - NavigationViewMetrics(progressViewSize.value, instructionsViewSize), - ), + buttonSize = theme.buttonSize, + cameraControlState = cameraControlState, showZoom = config.showZoom, - onClickZoomIn = { camera.value = camera.value.incrementZoom(1.0) }, - onClickZoomOut = { camera.value = camera.value.incrementZoom(-1.0) }) + onClickZoomIn = { config.onZoomIn?.invoke() }, + onClickZoomOut = { config.onZoomOut?.invoke() }, + bottomCenter = { + views.streetNameView(Modifier, uiState.currentStepRoadName, cameraControlState) + }) } } } @@ -119,8 +116,9 @@ fun LandscapeNavigationOverlayViewPreview() { LandscapeNavigationOverlayView( modifier = Modifier.fillMaxSize(), - camera = rememberSaveableMapViewCamera(), - navigationCamera = navigationMapViewCamera(), viewModel = viewModel, - onTapExit = {}) + cameraControlState = CameraControlState.Hidden, + mapViewInsets = remember { mutableStateOf(PaddingValues()) }, + onTapExit = {}, + ) } diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt new file mode 100644 index 00000000..25e2a454 --- /dev/null +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt @@ -0,0 +1,109 @@ +package com.stadiamaps.ferrostar.composeui.views.overlays + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.stadiamaps.ferrostar.composeui.config.NavigationViewComponentBuilder +import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig +import com.stadiamaps.ferrostar.composeui.models.CameraControlState +import com.stadiamaps.ferrostar.composeui.models.NavigationViewMetrics +import com.stadiamaps.ferrostar.composeui.theme.DefaultNavigationUITheme +import com.stadiamaps.ferrostar.composeui.theme.NavigationUITheme +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.NavigatingInnerGridView +import com.stadiamaps.ferrostar.core.NavigationUiState +import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.mock.MockNavigationViewModel +import com.stadiamaps.ferrostar.core.mock.pedestrianExample +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@Composable +fun PortraitNavigationOverlayView( + modifier: Modifier, + viewModel: NavigationViewModel, + cameraControlState: CameraControlState, + theme: NavigationUITheme = DefaultNavigationUITheme, + config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), + views: NavigationViewComponentBuilder = NavigationViewComponentBuilder.Default(theme), + mapViewInsets: MutableState, + onTapExit: (() -> Unit)? = null, +) { + val density = LocalDensity.current + val windowInsets = WindowInsets.statusBars.asPaddingValues() + + val uiState by viewModel.uiState.collectAsState() + + var instructionsViewSize by remember { mutableStateOf(DpSize.Zero) } + var progressViewSize by remember { mutableStateOf(DpSize.Zero) } + + mapViewInsets.value = + NavigationViewMetrics( + progressViewSize = progressViewSize, + instructionsViewSize = instructionsViewSize, + buttonSize = theme.buttonSize) + .mapViewInsets( + top = 32.dp + windowInsets.calculateTopPadding(), + bottom = 32.dp + windowInsets.calculateBottomPadding()) + + Column(modifier) { + views.instructionsView( + Modifier.onSizeChanged { + instructionsViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } + }, + uiState) + + NavigatingInnerGridView( + modifier = Modifier.fillMaxSize().weight(1f).padding(bottom = 16.dp, top = 16.dp), + showMute = config.showMute, + isMuted = uiState.isMuted, + onClickMute = { viewModel.toggleMute() }, + buttonSize = theme.buttonSize, + cameraControlState = cameraControlState, + showZoom = config.showZoom, + onClickZoomIn = { config.onZoomIn?.invoke() }, + onClickZoomOut = { config.onZoomOut?.invoke() }, + bottomCenter = { + views.streetNameView(Modifier, uiState.currentStepRoadName, cameraControlState) + }, + ) + + views.progressView( + Modifier.onSizeChanged { + progressViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } + }, + uiState, + onTapExit) + } +} + +@Composable +@Preview +fun PortraitNavigationOverlayViewPreview() { + val viewModel = + MockNavigationViewModel(MutableStateFlow(NavigationUiState.pedestrianExample()).asStateFlow()) + + PortraitNavigationOverlayView( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + cameraControlState = CameraControlState.Hidden, + mapViewInsets = remember { mutableStateOf(PaddingValues()) }, + onTapExit = {}, + ) +} diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InnerGridViewTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InnerGridViewTest.kt index a331a165..ea7f5e19 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InnerGridViewTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InnerGridViewTest.kt @@ -1,7 +1,7 @@ package com.stadiamaps.ferrostar.views -import com.stadiamaps.ferrostar.composeui.views.gridviews.InnerGridViewPreview -import com.stadiamaps.ferrostar.composeui.views.gridviews.InnerGridViewSampleLayoutPreview +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.InnerGridViewPreview +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.InnerGridViewSampleLayoutPreview import com.stadiamaps.ferrostar.support.paparazziDefault import com.stadiamaps.ferrostar.support.withSnapshotBackground import org.junit.Rule diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InstructionViewTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InstructionViewTest.kt index 4f356d16..6923ec94 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InstructionViewTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/InstructionViewTest.kt @@ -1,6 +1,6 @@ package com.stadiamaps.ferrostar.views -import com.stadiamaps.ferrostar.composeui.views.InstructionsView +import com.stadiamaps.ferrostar.composeui.views.components.InstructionsView import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.mock.pedestrianExample import com.stadiamaps.ferrostar.support.paparazziDefault diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/ManeuverImageTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/ManeuverImageTest.kt index aa89216a..2068c103 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/ManeuverImageTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/ManeuverImageTest.kt @@ -3,7 +3,7 @@ package com.stadiamaps.ferrostar.views import androidx.compose.ui.graphics.Color import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 import app.cash.paparazzi.Paparazzi -import com.stadiamaps.ferrostar.composeui.views.maneuver.ManeuverImage +import com.stadiamaps.ferrostar.composeui.views.components.maneuver.ManeuverImage import org.junit.Rule import org.junit.Test import uniffi.ferrostar.ManeuverModifier diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt index d4ed607c..053ce714 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigatingInnerGridViewTest.kt @@ -1,9 +1,9 @@ package com.stadiamaps.ferrostar.views -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewLandscapeNonTrackingPreview -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewLandscapeTrackingPreview -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewNonTrackingPreview -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridViewTrackingPreview +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.NavigatingInnerGridViewLandscapeNonTrackingPreview +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.NavigatingInnerGridViewLandscapeTrackingPreview +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.NavigatingInnerGridViewNonTrackingPreview +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.NavigatingInnerGridViewTrackingPreview import com.stadiamaps.ferrostar.support.paparazziDefault import com.stadiamaps.ferrostar.support.withSnapshotBackground import org.junit.Rule diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt index ca2669c1..2595c16d 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/NavigationUIButtonTest.kt @@ -10,8 +10,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIButton -import com.stadiamaps.ferrostar.composeui.views.controls.NavigationUIZoomButton +import com.stadiamaps.ferrostar.composeui.views.components.controls.NavigationUIButton +import com.stadiamaps.ferrostar.composeui.views.components.controls.NavigationUIZoomButton import com.stadiamaps.ferrostar.support.paparazziDefault import org.junit.Rule import org.junit.Test diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/RTLInstructionViewTests.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/RTLInstructionViewTests.kt index a1354381..3b2ca041 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/RTLInstructionViewTests.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/RTLInstructionViewTests.kt @@ -4,7 +4,7 @@ import android.icu.util.ULocale import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 import app.cash.paparazzi.Paparazzi import com.stadiamaps.ferrostar.composeui.formatting.LocalizedDistanceFormatter -import com.stadiamaps.ferrostar.composeui.views.InstructionsView +import com.stadiamaps.ferrostar.composeui.views.components.InstructionsView import com.stadiamaps.ferrostar.support.withSnapshotBackground import org.junit.Rule import org.junit.Test diff --git a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/TripProgressViewTest.kt b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/TripProgressViewTest.kt index 55ddc8f1..d22de4d8 100644 --- a/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/TripProgressViewTest.kt +++ b/android/composeui/src/test/java/com/stadiamaps/ferrostar/views/TripProgressViewTest.kt @@ -1,9 +1,9 @@ package com.stadiamaps.ferrostar.views -import com.stadiamaps.ferrostar.composeui.views.ProgressView24HourPreview -import com.stadiamaps.ferrostar.composeui.views.ProgressViewInformationalPreview -import com.stadiamaps.ferrostar.composeui.views.ProgressViewWithExitPreview -import com.stadiamaps.ferrostar.composeui.views.TripProgressView +import com.stadiamaps.ferrostar.composeui.views.components.ProgressView24HourPreview +import com.stadiamaps.ferrostar.composeui.views.components.ProgressViewInformationalPreview +import com.stadiamaps.ferrostar.composeui.views.components.ProgressViewWithExitPreview +import com.stadiamaps.ferrostar.composeui.views.components.TripProgressView import com.stadiamaps.ferrostar.support.paparazziDefault import com.stadiamaps.ferrostar.support.withSnapshotBackground import kotlinx.datetime.Instant diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionView.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionView.png index e427e067..18aaba51 100644 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionView.png and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionView.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionViewExpanded.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionViewExpanded.png index a4e3b9f7..497c39dc 100644 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionViewExpanded.png and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_InstructionViewTest_testInstructionViewExpanded.png differ diff --git a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_RTLInstructionViewTests_testRTLInstructionView.png b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_RTLInstructionViewTests_testRTLInstructionView.png index 2546b01e..27981ca7 100644 Binary files a/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_RTLInstructionViewTests_testRTLInstructionView.png and b/android/composeui/src/test/snapshots/images/com.stadiamaps.ferrostar.views_RTLInstructionViewTests_testRTLInstructionView.png differ diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AutocompleteOverlay.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AutocompleteOverlay.kt new file mode 100644 index 00000000..2c2bb4dc --- /dev/null +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AutocompleteOverlay.kt @@ -0,0 +1,62 @@ +package com.stadiamaps.ferrostar + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.stadiamaps.autocomplete.AutocompleteSearch +import com.stadiamaps.autocomplete.center +import com.stadiamaps.ferrostar.composeui.views.components.gridviews.InnerGridView +import com.stadiamaps.ferrostar.core.LocationProvider +import com.stadiamaps.ferrostar.core.SimulatedLocationProvider +import com.stadiamaps.ferrostar.core.toAndroidLocation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import uniffi.ferrostar.GeographicCoordinate +import uniffi.ferrostar.UserLocation +import uniffi.ferrostar.Waypoint +import uniffi.ferrostar.WaypointKind + +@Composable +fun AutocompleteOverlay( + modifier: Modifier = Modifier, + scope: CoroutineScope, + isNavigating: Boolean, + locationProvider: LocationProvider, + loc: UserLocation +) { + if (!isNavigating) { + InnerGridView( + modifier = modifier.fillMaxSize().padding(bottom = 16.dp, top = 16.dp), + topCenter = { + AppModule.stadiaApiKey?.let { apiKey -> + AutocompleteSearch(apiKey = apiKey, userLocation = loc.toAndroidLocation()) { feature -> + feature.center()?.let { center -> + // Fetch a route in the background + scope.launch(Dispatchers.IO) { + // TODO: Fail gracefully + val routes = + AppModule.ferrostarCore.getRoutes( + loc, + listOf( + Waypoint( + coordinate = + GeographicCoordinate(center.latitude, center.longitude), + kind = WaypointKind.BREAK), + )) + + val route = routes.first() + AppModule.ferrostarCore.startNavigation(route = route) + + if (locationProvider is SimulatedLocationProvider) { + locationProvider.setSimulatedRoute(route) + } + } + } + } + } + }) + } +} diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index 5a5aa259..248ad1bc 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -16,27 +16,20 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.mapbox.mapboxsdk.geometry.LatLng import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.rememberSaveableMapViewCamera import com.maplibre.compose.symbols.Circle -import com.stadiamaps.autocomplete.AutocompleteSearch import com.stadiamaps.autocomplete.center +import com.stadiamaps.ferrostar.composeui.config.NavigationViewComponentBuilder +import com.stadiamaps.ferrostar.composeui.config.withCustomOverlayView import com.stadiamaps.ferrostar.composeui.runtime.KeepScreenOnDisposableEffect -import com.stadiamaps.ferrostar.composeui.views.gridviews.InnerGridView import com.stadiamaps.ferrostar.core.AndroidSystemLocationProvider import com.stadiamaps.ferrostar.core.LocationProvider -import com.stadiamaps.ferrostar.core.SimulatedLocationProvider -import com.stadiamaps.ferrostar.core.toAndroidLocation import com.stadiamaps.ferrostar.googleplayservices.FusedLocationProvider import com.stadiamaps.ferrostar.maplibreui.views.DynamicallyOrientingNavigationView import kotlin.math.min -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import uniffi.ferrostar.GeographicCoordinate -import uniffi.ferrostar.Waypoint -import uniffi.ferrostar.WaypointKind @Composable fun DemoNavigationScene( @@ -113,42 +106,18 @@ fun DemoNavigationScene( // Snapping works well for most motor vehicle navigation. // Other travel modes though, such as walking, may not want snapping. snapUserLocationToRoute = false, - onTapExit = { viewModel.stopNavigation() }, - userContent = { modifier -> - if (!vmState.isNavigating()) { - InnerGridView( - modifier = modifier.fillMaxSize().padding(bottom = 16.dp, top = 16.dp), - topCenter = { - AppModule.stadiaApiKey?.let { apiKey -> - AutocompleteSearch(apiKey = apiKey, userLocation = loc.toAndroidLocation()) { - feature -> - feature.center()?.let { center -> - // Fetch a route in the background - scope.launch(Dispatchers.IO) { - // TODO: Fail gracefully - val routes = - AppModule.ferrostarCore.getRoutes( - loc, - listOf( - Waypoint( - coordinate = - GeographicCoordinate(center.latitude, center.longitude), - kind = WaypointKind.BREAK), - )) - - val route = routes.first() - AppModule.ferrostarCore.startNavigation(route = route) - - if (locationProvider is SimulatedLocationProvider) { - locationProvider.setSimulatedRoute(route) - } - } - } - } - } - }) - } - }) { uiState -> + views = + NavigationViewComponentBuilder.Default() + .withCustomOverlayView( + customOverlayView = { modifier -> + AutocompleteOverlay( + modifier = modifier, + scope = scope, + isNavigating = vmState.isNavigating(), + locationProvider = locationProvider, + loc = loc) + }), + onTapExit = { viewModel.stopNavigation() }) { uiState -> // Trivial, if silly example of how to add your own overlay layers. // (Also incidentally highlights the lag inherent in MapLibre location tracking // as-is.) diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 6b5df06d..8f4eb923 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -14,7 +14,7 @@ androidx-activity-compose = "1.9.3" compose = "2024.10.00" okhttp = "4.12.0" moshi = "1.15.1" -maplibre-compose = "0.3.0" +maplibre-compose = "0.4.0" playServicesLocation = "21.3.0" junit = "4.13.2" junitVersion = "1.2.1" diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationViewMetrics.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationViewMetrics.kt deleted file mode 100644 index f9a55802..00000000 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationViewMetrics.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.stadiamaps.ferrostar.maplibreui - -import androidx.compose.ui.unit.DpSize - -data class NavigationViewMetrics( - val progressViewSize: DpSize, - val instructionsViewSize: DpSize, -) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/extensions/VisualNavigationViewConfig.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/extensions/VisualNavigationViewConfig.kt index 0821f916..a35fed83 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/extensions/VisualNavigationViewConfig.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/extensions/VisualNavigationViewConfig.kt @@ -1,65 +1,40 @@ package com.stadiamaps.ferrostar.maplibreui.extensions +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.unit.LayoutDirection import com.mapbox.mapboxsdk.geometry.LatLngBounds import com.maplibre.compose.camera.CameraState import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.camera.models.CameraPadding -import com.stadiamaps.ferrostar.composeui.config.CameraControlState import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig -import com.stadiamaps.ferrostar.core.NavigationUiState -import com.stadiamaps.ferrostar.core.boundingBox -import com.stadiamaps.ferrostar.maplibreui.NavigationViewMetrics +import com.stadiamaps.ferrostar.composeui.models.CameraControlState +import com.stadiamaps.ferrostar.core.BoundingBox @Composable fun VisualNavigationViewConfig.cameraControlState( camera: MutableState, navigationCamera: MapViewCamera, - uiState: NavigationUiState, - navigationViewMetrics: NavigationViewMetrics + mapViewInsets: PaddingValues, + boundingBox: BoundingBox? ): CameraControlState { val cameraIsTrackingLocation = camera.value.state is CameraState.TrackingUserLocationWithBearing - val cameraControlState = - if (!cameraIsTrackingLocation) { - CameraControlState.ShowRecenter { camera.value = navigationCamera } - } else { - val bbox = uiState.routeGeometry?.boundingBox() - if (bbox != null) { - val scale = LocalDensity.current.density - val progressViewHeight = navigationViewMetrics.progressViewSize.height.value.toDouble() - val instructionsViewHeight = - navigationViewMetrics.instructionsViewSize.height.value.toDouble() - val layoutDirection = LocalLayoutDirection.current + val cameraPadding = CameraPadding.padding(mapViewInsets) - // Bottom padding must take the recenter button into account - val bottomPadding = (progressViewHeight + this.buttonSize.height.value + 50) * scale - // The top padding needs to take the puck into account - val topPadding = (instructionsViewHeight + 75) * scale - val (startPadding, endPadding) = - when (layoutDirection) { - LayoutDirection.Ltr -> 20.0 * scale to (this.buttonSize.width.value + 50) * scale - - LayoutDirection.Rtl -> (this.buttonSize.width.value + 50) * scale to 20.0 * scale - } - - CameraControlState.ShowRouteOverview { - camera.value = - MapViewCamera.BoundingBox( - LatLngBounds.from(bbox.north, bbox.east, bbox.south, bbox.west), - padding = - CameraPadding( - startPadding.toDouble(), - topPadding, - endPadding.toDouble(), - bottomPadding)) - } - } else { - CameraControlState.Hidden - } + return if (!cameraIsTrackingLocation) { + CameraControlState.ShowRecenter { camera.value = navigationCamera } + } else { + if (boundingBox != null) { + CameraControlState.ShowRouteOverview { + camera.value = + MapViewCamera.BoundingBox( + bounds = + LatLngBounds.from( + boundingBox.north, boundingBox.east, boundingBox.south, boundingBox.west), + padding = cameraPadding) } - return cameraControlState + } else { + CameraControlState.Hidden + } + } } diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/MapControls.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/MapControls.kt index ade21236..527c0c4d 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/MapControls.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/MapControls.kt @@ -11,9 +11,9 @@ import androidx.compose.runtime.State import androidx.compose.runtime.produceState import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.maplibre.compose.runtime.localLayoutDirection import com.maplibre.compose.settings.AttributionSettings import com.maplibre.compose.settings.CompassSettings import com.maplibre.compose.settings.LogoSettings @@ -41,7 +41,7 @@ internal fun rememberMapControlsForProgressViewHeight( horizontalPadding: Dp = 16.dp, verticalPadding: Dp = 8.dp ): State { - val layoutDirection = localLayoutDirection() + val layoutDirection = LocalLayoutDirection.current val density = LocalDensity.current val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/NavigationCamera.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/NavigationCamera.kt index 5f772b6f..b5948e3d 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/NavigationCamera.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/runtime/NavigationCamera.kt @@ -4,7 +4,7 @@ import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalConfiguration import com.maplibre.compose.camera.MapViewCamera -import com.maplibre.compose.camera.cameraPaddingFractionOfScreen +import com.maplibre.compose.camera.models.CameraPadding sealed class NavigationActivity(val zoom: Double, val pitch: Double) { /** The recommended camera configuration for automotive navigation. */ @@ -32,7 +32,7 @@ fun navigationMapViewCamera( val screenOrientation = LocalConfiguration.current.orientation val start = if (screenOrientation == Configuration.ORIENTATION_LANDSCAPE) 0.5f else 0.0f - val cameraPadding = cameraPaddingFractionOfScreen(start = start, top = 0.5f) + val cameraPadding = CameraPadding.fractionOfScreen(start = start, top = 0.5f) return MapViewCamera.TrackingUserLocationWithBearing( zoom = activity.zoom, pitch = activity.pitch, padding = cameraPadding) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt index b51f9e42..96ea05e9 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt @@ -2,8 +2,7 @@ package com.stadiamaps.ferrostar.maplibreui.views import android.content.res.Configuration import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -23,17 +22,21 @@ import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.ramani.LocationRequestProperties import com.maplibre.compose.ramani.MapLibreComposable import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.NavigationViewComponentBuilder import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.runtime.paddingForGridView -import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView +import com.stadiamaps.ferrostar.composeui.theme.DefaultNavigationUITheme +import com.stadiamaps.ferrostar.composeui.theme.NavigationUITheme +import com.stadiamaps.ferrostar.composeui.views.overlays.LandscapeNavigationOverlayView +import com.stadiamaps.ferrostar.composeui.views.overlays.PortraitNavigationOverlayView import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.boundingBox import com.stadiamaps.ferrostar.maplibreui.NavigationMapView import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault +import com.stadiamaps.ferrostar.maplibreui.extensions.cameraControlState import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import com.stadiamaps.ferrostar.maplibreui.runtime.rememberMapControlsForProgressViewHeight -import com.stadiamaps.ferrostar.maplibreui.views.overlays.LandscapeNavigationOverlayView -import com.stadiamaps.ferrostar.maplibreui.views.overlays.PortraitNavigationOverlayView /** * A dynamically orienting navigation view that switches between portrait and landscape orientations @@ -49,11 +52,11 @@ import com.stadiamaps.ferrostar.maplibreui.views.overlays.PortraitNavigationOver * engine. * @param snapUserLocationToRoute If true, the user's displayed location will be snapped to the * route line. + * @param theme The navigation UI theme to use for the view. * @param config The configuration for the navigation view. + * @param views The navigation view component builder to use for the view. + * @param mapViewInsets The padding inset representing the open area of the map. * @param onTapExit The callback to invoke when the exit button is tapped. - * @param userContent Any composable with additional content to render. The most common use of this - * parameter is to display custom UI when there is no navigation in progress. See the demo app for - * an example that adds a search box. * @param mapContent Any additional composable map symbol content to render. */ @Composable @@ -66,15 +69,11 @@ fun DynamicallyOrientingNavigationView( locationRequestProperties: LocationRequestProperties = LocationRequestProperties.NavigationDefault(), snapUserLocationToRoute: Boolean = true, + theme: NavigationUITheme = DefaultNavigationUITheme, config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), - currentRoadNameView: @Composable (String?) -> Unit = { roadName -> - if (roadName != null) { - CurrentRoadNameView(roadName) - Spacer(modifier = Modifier.height(8.dp)) - } - }, + views: NavigationViewComponentBuilder = NavigationViewComponentBuilder.Default(theme), + mapViewInsets: MutableState = remember { mutableStateOf(PaddingValues(0.dp)) }, onTapExit: (() -> Unit)? = null, - userContent: @Composable (BoxScope.(Modifier) -> Unit)? = null, mapContent: @Composable @MapLibreComposable ((NavigationUiState) -> Unit)? = null, ) { val orientation = LocalConfiguration.current.orientation @@ -106,31 +105,43 @@ fun DynamicallyOrientingNavigationView( Configuration.ORIENTATION_LANDSCAPE -> { LandscapeNavigationOverlayView( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), - camera = camera, - navigationCamera = navigationCamera, viewModel = viewModel, + cameraControlState = + config.cameraControlState( + camera = camera, + navigationCamera = navigationCamera, + mapViewInsets = mapViewInsets.value, + boundingBox = uiState.routeGeometry?.boundingBox(), + ), + theme = theme, config = config, - progressViewSize = rememberProgressViewSize, - onTapExit = onTapExit, - currentRoadNameView = currentRoadNameView) + views = views, + mapViewInsets = mapViewInsets, + onTapExit = onTapExit) } else -> { PortraitNavigationOverlayView( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), - camera = camera, - navigationCamera = navigationCamera, viewModel = viewModel, + cameraControlState = + config.cameraControlState( + camera = camera, + navigationCamera = navigationCamera, + mapViewInsets = mapViewInsets.value, + boundingBox = uiState.routeGeometry?.boundingBox(), + ), + theme = theme, config = config, - progressViewSize = rememberProgressViewSize, - onTapExit = onTapExit, - currentRoadNameView = currentRoadNameView) + views = views, + mapViewInsets = mapViewInsets, + onTapExit = onTapExit) } } } - if (userContent != null) { - userContent(Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding)) + views.customOverlayView?.let { customOverlayView -> + customOverlayView(Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding)) } } } diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt index baf6d6ff..8a28bfe6 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/LandscapeNavigationView.kt @@ -1,10 +1,9 @@ package com.stadiamaps.ferrostar.maplibreui.views import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding @@ -12,6 +11,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -19,18 +20,22 @@ import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.ramani.LocationRequestProperties import com.maplibre.compose.ramani.MapLibreComposable import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.NavigationViewComponentBuilder import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.runtime.paddingForGridView -import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView +import com.stadiamaps.ferrostar.composeui.theme.DefaultNavigationUITheme +import com.stadiamaps.ferrostar.composeui.theme.NavigationUITheme +import com.stadiamaps.ferrostar.composeui.views.overlays.LandscapeNavigationOverlayView import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.boundingBox import com.stadiamaps.ferrostar.core.mock.MockNavigationViewModel import com.stadiamaps.ferrostar.core.mock.pedestrianExample import com.stadiamaps.ferrostar.maplibreui.NavigationMapView import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault +import com.stadiamaps.ferrostar.maplibreui.extensions.cameraControlState import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import com.stadiamaps.ferrostar.maplibreui.runtime.rememberMapControlsForProgressViewHeight -import com.stadiamaps.ferrostar.maplibreui.views.overlays.LandscapeNavigationOverlayView import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -48,10 +53,12 @@ import kotlinx.coroutines.flow.asStateFlow * engine. * @param snapUserLocationToRoute If true, the user's displayed location will be snapped to the * route line. + * @param theme The navigation UI theme to use for the view. * @param config The configuration for the navigation view. - * @param overlayModifier The modifier to apply to the overlay view. + * @param views The navigation view component builder to use for the view. + * @param mapViewInsets The padding inset representing the open area of the map. * @param onTapExit The callback to invoke when the exit button is tapped. - * @param content Any additional composable map symbol content to render. + * @param mapContent Any additional composable map symbol content to render. */ @Composable fun LandscapeNavigationView( @@ -63,15 +70,12 @@ fun LandscapeNavigationView( locationRequestProperties: LocationRequestProperties = LocationRequestProperties.NavigationDefault(), snapUserLocationToRoute: Boolean = true, + theme: NavigationUITheme = DefaultNavigationUITheme, config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), - currentRoadNameView: @Composable (String?) -> Unit = { roadName -> - if (roadName != null) { - CurrentRoadNameView(roadName) - Spacer(modifier = Modifier.height(8.dp)) - } - }, + views: NavigationViewComponentBuilder = NavigationViewComponentBuilder.Default(theme), + mapViewInsets: MutableState = remember { mutableStateOf(PaddingValues(0.dp)) }, onTapExit: (() -> Unit)? = null, - content: @Composable @MapLibreComposable() ((NavigationUiState) -> Unit)? = null, + mapContent: @Composable @MapLibreComposable() ((NavigationUiState) -> Unit)? = null, ) { val uiState by viewModel.uiState.collectAsState() @@ -90,16 +94,27 @@ fun LandscapeNavigationView( locationRequestProperties, snapUserLocationToRoute, onMapReadyCallback = { camera.value = navigationCamera }, - content) + mapContent) LandscapeNavigationOverlayView( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), - config = config, - camera = camera, - navigationCamera = navigationCamera, viewModel = viewModel, - onTapExit = onTapExit, - currentRoadNameView = currentRoadNameView) + cameraControlState = + config.cameraControlState( + camera = camera, + navigationCamera = navigationCamera, + mapViewInsets = mapViewInsets.value, + boundingBox = uiState.routeGeometry?.boundingBox(), + ), + theme = theme, + config = config, + views = views, + mapViewInsets = mapViewInsets, + onTapExit = onTapExit) + + views.customOverlayView?.let { customOverlayView -> + customOverlayView(Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding)) + } } } diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt index f7ba7ff5..f7e29ea2 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/PortraitNavigationView.kt @@ -1,14 +1,15 @@ package com.stadiamaps.ferrostar.maplibreui.views +import android.util.Log import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -17,24 +18,27 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import com.maplibre.compose.camera.MapViewCamera import com.maplibre.compose.ramani.LocationRequestProperties import com.maplibre.compose.ramani.MapLibreComposable import com.maplibre.compose.rememberSaveableMapViewCamera +import com.stadiamaps.ferrostar.composeui.config.NavigationViewComponentBuilder import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig import com.stadiamaps.ferrostar.composeui.runtime.paddingForGridView -import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView +import com.stadiamaps.ferrostar.composeui.theme.DefaultNavigationUITheme +import com.stadiamaps.ferrostar.composeui.theme.NavigationUITheme +import com.stadiamaps.ferrostar.composeui.views.overlays.PortraitNavigationOverlayView import com.stadiamaps.ferrostar.core.NavigationUiState import com.stadiamaps.ferrostar.core.NavigationViewModel +import com.stadiamaps.ferrostar.core.boundingBox import com.stadiamaps.ferrostar.core.mock.MockNavigationViewModel import com.stadiamaps.ferrostar.core.mock.pedestrianExample import com.stadiamaps.ferrostar.maplibreui.NavigationMapView import com.stadiamaps.ferrostar.maplibreui.extensions.NavigationDefault +import com.stadiamaps.ferrostar.maplibreui.extensions.cameraControlState import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera import com.stadiamaps.ferrostar.maplibreui.runtime.rememberMapControlsForProgressViewHeight -import com.stadiamaps.ferrostar.maplibreui.views.overlays.PortraitNavigationOverlayView import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -52,9 +56,12 @@ import kotlinx.coroutines.flow.asStateFlow * engine. * @param snapUserLocationToRoute If true, the user's displayed location will be snapped to the * route line. + * @param theme The navigation UI theme to use for the view. * @param config The configuration for the navigation view. + * @param views The navigation view component builder to use for the view. + * @param mapViewInsets The padding inset representing the open area of the map. * @param onTapExit The callback to invoke when the exit button is tapped. - * @param content Any additional composable map symbol content to render. + * @param mapContent Any additional composable map symbol content to render. */ @Composable fun PortraitNavigationView( @@ -66,27 +73,26 @@ fun PortraitNavigationView( locationRequestProperties: LocationRequestProperties = LocationRequestProperties.NavigationDefault(), snapUserLocationToRoute: Boolean = true, + theme: NavigationUITheme = DefaultNavigationUITheme, config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), - currentRoadNameView: @Composable (String?) -> Unit = { roadName -> - if (roadName != null) { - CurrentRoadNameView(roadName) - Spacer(modifier = Modifier.height(8.dp)) - } - }, + views: NavigationViewComponentBuilder = NavigationViewComponentBuilder.Default(theme), + mapViewInsets: MutableState = remember { mutableStateOf(PaddingValues(0.dp)) }, onTapExit: (() -> Unit)? = null, - content: @Composable @MapLibreComposable() ((NavigationUiState) -> Unit)? = null, + mapContent: @Composable @MapLibreComposable() ((NavigationUiState) -> Unit)? = null, ) { val uiState by viewModel.uiState.collectAsState() + LaunchedEffect(mapViewInsets.value) { + Log.d("PortraitNavigationView", "mapViewInsets.value: ${mapViewInsets.value}") + } + // Get the correct padding based on edge-to-edge status. val gridPadding = paddingForGridView() - // Maintain the actual size of the progress view for MapControl layout purposes. - val rememberProgressViewSize = remember { mutableStateOf(DpSize.Zero) } - val progressViewSize by rememberProgressViewSize - // Get the map control positioning based on the progress view. - val mapControls = rememberMapControlsForProgressViewHeight(progressViewSize.height) + // TODO: I think we should just remove all annotations for nav & make a better tool if needed. + // val mapControls = rememberMapControlsForProgressViewHeight(progressViewSize.height) + val mapControls = rememberMapControlsForProgressViewHeight() Box(modifier) { NavigationMapView( @@ -98,18 +104,29 @@ fun PortraitNavigationView( locationRequestProperties, snapUserLocationToRoute, onMapReadyCallback = { camera.value = navigationCamera }, - content) + mapContent) if (uiState.isNavigating()) { PortraitNavigationOverlayView( modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), - config = config, - camera = camera, - navigationCamera = navigationCamera, viewModel = viewModel, - progressViewSize = rememberProgressViewSize, - onTapExit = onTapExit, - currentRoadNameView = currentRoadNameView) + cameraControlState = + config.cameraControlState( + camera = camera, + navigationCamera = navigationCamera, + mapViewInsets = mapViewInsets.value, + boundingBox = uiState.routeGeometry?.boundingBox(), + ), + theme = theme, + config = config, + views = views, + mapViewInsets = mapViewInsets, + onTapExit = onTapExit) + + views.customOverlayView?.let { customOverlayView -> + customOverlayView( + Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding)) + } } } } diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt deleted file mode 100644 index 596238a4..00000000 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.stadiamaps.ferrostar.maplibreui.views.overlays - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import com.maplibre.compose.camera.CameraState -import com.maplibre.compose.camera.MapViewCamera -import com.maplibre.compose.camera.extensions.incrementZoom -import com.maplibre.compose.rememberSaveableMapViewCamera -import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig -import com.stadiamaps.ferrostar.composeui.views.CurrentRoadNameView -import com.stadiamaps.ferrostar.composeui.views.InstructionsView -import com.stadiamaps.ferrostar.composeui.views.TripProgressView -import com.stadiamaps.ferrostar.composeui.views.gridviews.NavigatingInnerGridView -import com.stadiamaps.ferrostar.core.NavigationUiState -import com.stadiamaps.ferrostar.core.NavigationViewModel -import com.stadiamaps.ferrostar.core.mock.MockNavigationViewModel -import com.stadiamaps.ferrostar.core.mock.pedestrianExample -import com.stadiamaps.ferrostar.maplibreui.NavigationViewMetrics -import com.stadiamaps.ferrostar.maplibreui.extensions.cameraControlState -import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow - -@Composable -fun PortraitNavigationOverlayView( - modifier: Modifier, - camera: MutableState, - navigationCamera: MapViewCamera, - viewModel: NavigationViewModel, - config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), - progressViewSize: MutableState = remember { mutableStateOf(DpSize.Zero) }, - onTapExit: (() -> Unit)? = null, - currentRoadNameView: @Composable (String?) -> Unit = { roadName -> - if (roadName != null) { - CurrentRoadNameView(roadName) - Spacer(modifier = Modifier.height(8.dp)) - } - }, -) { - val density = LocalDensity.current - val uiState by viewModel.uiState.collectAsState() - var instructionsViewSize by remember { mutableStateOf(DpSize.Zero) } - - Column(modifier) { - uiState.visualInstruction?.let { instructions -> - InstructionsView( - instructions, - modifier = - Modifier.onSizeChanged { - instructionsViewSize = density.run { DpSize(it.width.toDp(), it.height.toDp()) } - }, - remainingSteps = uiState.remainingSteps, - distanceToNextManeuver = uiState.progress?.distanceToNextManeuver) - } - - val cameraIsTrackingLocation = camera.value.state is CameraState.TrackingUserLocationWithBearing - - NavigatingInnerGridView( - modifier = Modifier.fillMaxSize().weight(1f).padding(bottom = 16.dp, top = 16.dp), - showMute = config.showMute, - isMuted = uiState.isMuted, - onClickMute = { viewModel.toggleMute() }, - buttonSize = config.buttonSize, - cameraControlState = - config.cameraControlState( - camera, - navigationCamera, - uiState, - NavigationViewMetrics(progressViewSize.value, instructionsViewSize), - ), - showZoom = config.showZoom, - onClickZoomIn = { camera.value = camera.value.incrementZoom(1.0) }, - onClickZoomOut = { camera.value = camera.value.incrementZoom(-1.0) }, - ) - - uiState.progress?.let { progress -> - Column(horizontalAlignment = Alignment.CenterHorizontally) { - val currentRoadName = - if (cameraIsTrackingLocation) { - uiState.currentStepRoadName - } else { - // Hide the road name view if not tracking the user location - null - } - currentRoadName?.let { roadName -> currentRoadNameView(roadName) } - TripProgressView( - modifier = - Modifier.onSizeChanged { - progressViewSize.value = density.run { DpSize(it.width.toDp(), it.height.toDp()) } - }, - progress = progress, - onTapExit = onTapExit) - } - } - } -} - -@Composable -@Preview -fun PortraitNavigationOverlayViewPreview() { - val viewModel = - MockNavigationViewModel(MutableStateFlow(NavigationUiState.pedestrianExample()).asStateFlow()) - - PortraitNavigationOverlayView( - modifier = Modifier.fillMaxSize(), - camera = rememberSaveableMapViewCamera(), - navigationCamera = navigationMapViewCamera(), - viewModel = viewModel, - onTapExit = {}) -} diff --git a/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt b/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt index 6d5c247d..698ce3b3 100644 --- a/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt +++ b/android/maplibreui/src/test/java/com/stadiamaps/ferrostar/maplibreui/config/VisualNavigationViewConfigTest.kt @@ -1,12 +1,8 @@ package com.stadiamaps.ferrostar.maplibreui.config -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp import com.stadiamaps.ferrostar.composeui.config.VisualNavigationViewConfig -import com.stadiamaps.ferrostar.composeui.config.buttonSize import com.stadiamaps.ferrostar.composeui.config.useMuteButton import com.stadiamaps.ferrostar.composeui.config.useZoomButton -import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Test @@ -21,34 +17,30 @@ class VisualNavigationViewConfigTest { @Test fun testDefault() { - val config = VisualNavigationViewConfig.Default() + val config = VisualNavigationViewConfig.Companion.Default() assert(config.showMute) assert(config.showZoom) } @Test fun testUseMuteButton() { - val config = VisualNavigationViewConfig().useMuteButton() + val config = VisualNavigationViewConfig().useMuteButton(onMute = {}) assert(config.showMute) } @Test fun testUseZoomButton() { - val config = VisualNavigationViewConfig().useZoomButton() + val config = VisualNavigationViewConfig().useZoomButton(onZoomIn = {}, onZoomOut = {}) assert(config.showZoom) } @Test fun testUseMuteButtonAndZoomButton() { - val config = VisualNavigationViewConfig().useMuteButton().useZoomButton() + val config = + VisualNavigationViewConfig() + .useMuteButton(onMute = {}) + .useZoomButton(onZoomIn = {}, onZoomOut = {}) assert(config.showMute) assert(config.showZoom) } - - @Test - fun testButtonSize() { - val newSize = DpSize(42.dp, 42.dp) - val config = VisualNavigationViewConfig().buttonSize(newSize) - assertEquals(newSize, config.buttonSize) - } }