From 12a6f199a2e0e0408c6b68338eebd43cd148bb40 Mon Sep 17 00:00:00 2001 From: Jacob Fielding Date: Wed, 13 Nov 2024 10:52:15 -0800 Subject: [PATCH] Feat/android/view refactor (#356) * feat: view refactor & cleanup * feat: refactoring views for better reuse, easier config and more general purpose features * feat: in progressing setting up basics for car play * Apply automatic changes * feat: in progressing setting up basics for car play * feat: improved android view config and setup, current road name and showcase route: * feat: improved android view config and setup, current road name and showcase route: * feat: improved android view config and setup, current road name and showcase route: * feat: improved android view config and setup, current road name and showcase route: --------- Co-authored-by: Archdoog --- .../config/NavigationViewComponentBuilder.kt | 103 ++++++++++++++ .../config/VisualNavigationViewConfig.kt | 37 ++--- .../composeui/models/CameraControlState.kt | 9 ++ .../composeui/models/NavigationViewMetrics.kt | 36 +++++ .../DefaultForegroundNotificationBuilder.kt | 2 +- .../composeui/theme/InstructionRowTheme.kt | 2 +- .../composeui/theme/NavigationUITheme.kt | 26 ++++ .../views/{ => components}/CurrentRoadView.kt | 2 +- .../{ => components}/InstructionsView.kt | 38 +++--- .../{ => components}/TripProgressView.kt | 2 +- .../controls/NavigationUIButton.kt | 2 +- .../controls/NavigationUIZoomButton.kt | 2 +- .../controls/PillDragHandle.kt | 4 +- .../gridviews/InnerGridView.kt | 2 +- .../gridviews/NavigatingInnerGridView.kt | 10 +- .../maneuver/ManeuverImage.kt | 2 +- .../maneuver/ManeuverInstructionView.kt | 2 +- .../LandscapeNavigationOverlayView.kt | 104 +++++++------- .../overlays/PortraitNavigationOverlayView.kt | 109 +++++++++++++++ .../ferrostar/views/InnerGridViewTest.kt | 4 +- .../ferrostar/views/InstructionViewTest.kt | 2 +- .../ferrostar/views/ManeuverImageTest.kt | 2 +- .../views/NavigatingInnerGridViewTest.kt | 8 +- .../ferrostar/views/NavigationUIButtonTest.kt | 4 +- .../views/RTLInstructionViewTests.kt | 2 +- .../ferrostar/views/TripProgressViewTest.kt | 8 +- ...nstructionViewTest_testInstructionView.png | Bin 9643 -> 9679 bytes ...onViewTest_testInstructionViewExpanded.png | Bin 9923 -> 10004 bytes ...uctionViewTests_testRTLInstructionView.png | Bin 8964 -> 9004 bytes .../ferrostar/AutocompleteOverlay.kt | 62 +++++++++ .../ferrostar/DemoNavigationScene.kt | 59 ++------ android/gradle/libs.versions.toml | 2 +- .../maplibreui/NavigationViewMetrics.kt | 8 -- .../extensions/VisualNavigationViewConfig.kt | 67 +++------ .../maplibreui/runtime/MapControls.kt | 4 +- .../maplibreui/runtime/NavigationCamera.kt | 4 +- .../DynamicallyOrientingNavigationView.kt | 65 +++++---- .../views/LandscapeNavigationView.kt | 53 +++++--- .../views/PortraitNavigationView.kt | 67 +++++---- .../overlays/PortraitNavigationOverlayView.kt | 127 ------------------ .../config/VisualNavigationViewConfigTest.kt | 22 +-- 41 files changed, 620 insertions(+), 444 deletions(-) create mode 100644 android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/config/NavigationViewComponentBuilder.kt create mode 100644 android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/CameraControlState.kt create mode 100644 android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/models/NavigationViewMetrics.kt create mode 100644 android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/theme/NavigationUITheme.kt rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/CurrentRoadView.kt (97%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/InstructionsView.kt (83%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/TripProgressView.kt (99%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/controls/NavigationUIButton.kt (96%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/controls/NavigationUIZoomButton.kt (97%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/controls/PillDragHandle.kt (93%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/gridviews/InnerGridView.kt (98%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/gridviews/NavigatingInnerGridView.kt (92%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/maneuver/ManeuverImage.kt (97%) rename android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/{ => components}/maneuver/ManeuverInstructionView.kt (97%) rename android/{maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui => composeui/src/main/java/com/stadiamaps/ferrostar/composeui}/views/overlays/LandscapeNavigationOverlayView.kt (51%) create mode 100644 android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/overlays/PortraitNavigationOverlayView.kt create mode 100644 android/demo-app/src/main/java/com/stadiamaps/ferrostar/AutocompleteOverlay.kt delete mode 100644 android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationViewMetrics.kt delete mode 100644 android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt 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 e427e067a8257a535c878afbe00dfc55cf6ccf22..18aaba512db1f505fe946327e40f6779f2a0aaca 100644 GIT binary patch literal 9679 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 delta 6976 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 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 2546b01e49f45f159ea295c1786f0c7cd928b9ce..27981ca77cb582fff328d5a7c18fc58328daa321 100644 GIT binary patch delta 6380 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 T7hz-x@n>UcXHoUn-N*j}963=x delta 6368 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 + 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) - } }