Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upcoming instructions android #326

Merged
merged 10 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ object DefaultInstructionRowTheme : InstructionRowTheme {
@Composable get() = MaterialTheme.colorScheme.onSurface

override val backgroundColor: Color
@Composable get() = MaterialTheme.colorScheme.surface
@Composable get() = MaterialTheme.colorScheme.surfaceContainerLow
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW this came from the Material 3 spec

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only comment on this is:

When material theme is used, the more complex the value is, the less likely someone has it in their theme. This isn't so much a problem. More just a warning when someone asks why their instructions row isn't changing color. It'll be because people rarely set those very nuanced values.

}
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
package com.stadiamaps.ferrostar.composeui.views

import android.icu.util.ULocale
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
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
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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 uniffi.ferrostar.ManeuverModifier
import uniffi.ferrostar.ManeuverType
import uniffi.ferrostar.RouteStep
import uniffi.ferrostar.VisualInstruction
import uniffi.ferrostar.VisualInstructionContent

Expand All @@ -36,23 +55,72 @@ fun InstructionsView(
distanceToNextManeuver: Double?,
distanceFormatter: DistanceFormatter = LocalizedDistanceFormatter(),
theme: InstructionRowTheme = DefaultInstructionRowTheme,
content: @Composable () -> Unit = {
ManeuverImage(instructions.primaryContent, tint = MaterialTheme.colorScheme.primary)
remainingSteps: List<RouteStep>? = null,
initExpanded: Boolean = false,
contentBuilder: @Composable (VisualInstruction) -> Unit = {
ManeuverImage(it.primaryContent, tint = MaterialTheme.colorScheme.primary)
}
) {
var isExpanded by remember { mutableStateOf(initExpanded) }
val screenHeight = LocalConfiguration.current.screenHeightDp.dp

Column(
modifier =
Modifier.fillMaxWidth()
.shadow(elevation = 5.dp, RoundedCornerShape(10.dp))
.heightIn(max = screenHeight)
.animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessHigh))
.background(theme.backgroundColor, RoundedCornerShape(10.dp))
.padding(8.dp)) {
ManeuverInstructionView(
text = instructions.primaryContent.text,
distanceFormatter = distanceFormatter,
distanceToNextManeuver = distanceToNextManeuver,
theme = theme,
content = content)
// TODO: Secondary instructions
.padding(16.dp)
.clickable {
// This makes the entire view a click target for expansion.
// If only the pill is a click target, you need to be a ninja to tap it.
isExpanded = true
}) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
// Primary content
item {
ManeuverInstructionView(
text = instructions.primaryContent.text,
distanceFormatter = distanceFormatter,
distanceToNextManeuver = distanceToNextManeuver,
theme = theme) {
contentBuilder(instructions)
}
}

// TODO: Secondary content

// Expanded content
if (isExpanded && remainingSteps != null && remainingSteps.count() > 1) {
item { HorizontalDivider(thickness = 1.dp) }
items(remainingSteps.drop(1)) { step ->
step.visualInstructions.firstOrNull()?.let { upcomingInstruction ->
Spacer(modifier = Modifier.height(8.dp))
ManeuverInstructionView(
text = upcomingInstruction.primaryContent.text,
distanceFormatter = distanceFormatter,
distanceToNextManeuver = step.distance,
theme = theme) {
contentBuilder(upcomingInstruction)
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(thickness = 1.dp)
}
}
ianthetechie marked this conversation as resolved.
Show resolved Hide resolved
}
}

if (isExpanded) {
Spacer(modifier = Modifier.weight(1.0f))
}

PillDragHandle(
isExpanded,
// The modifier here lets us keep the container as slim as possible
modifier = Modifier.offset(y = 4.dp).align(Alignment.CenterHorizontally),
iconTintColor = theme.iconTintColor) {
isExpanded = !isExpanded
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.stadiamaps.ferrostar.composeui.views.controls

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun PillDragHandle(
isExpanded: Boolean,
modifier: Modifier = Modifier.fillMaxWidth(),
iconTintColor: Color = MaterialTheme.colorScheme.onSurface,
toggle: () -> Unit = {}
) {
val handleHeight = if (isExpanded) 36.dp else 4.dp
Box(modifier = modifier.height(handleHeight).clickable(onClick = toggle)) {
if (isExpanded) {
Icon(
Icons.Rounded.KeyboardArrowUp,
modifier = Modifier.align(Alignment.Center),
contentDescription = "Show upcoming maneuvers",
tint = iconTintColor)
} else {
Box(
modifier =
Modifier.align(Alignment.Center)
.height(handleHeight)
.width(24.dp)
.background(iconTintColor, RoundedCornerShape(6.dp))
.semantics {
role = Role.Button
onClick(label = "Hide upcoming maneuvers") {
toggle()
true
}
})
}
}
}

@Preview
@Composable
fun PreviewPillDragHandleCollapsed() {
PillDragHandle(isExpanded = false, iconTintColor = Color.White)
}

@Preview
@Composable
fun PreviewPillDragHandleExpanded() {
PillDragHandle(isExpanded = true, iconTintColor = Color.White)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.stadiamaps.ferrostar.views

import com.stadiamaps.ferrostar.composeui.views.InstructionsView
import com.stadiamaps.ferrostar.core.NavigationUiState
import com.stadiamaps.ferrostar.core.mock.pedestrianExample
import com.stadiamaps.ferrostar.support.paparazziDefault
import com.stadiamaps.ferrostar.support.withSnapshotBackground
import org.junit.Rule
Expand Down Expand Up @@ -35,4 +37,19 @@ class InstructionViewTest {
}
}
}

@Test
fun testInstructionViewExpanded() {
val state = NavigationUiState.pedestrianExample()

paparazzi.snapshot {
withSnapshotBackground {
InstructionsView(
instructions = state.visualInstruction!!,
remainingSteps = state.remainingSteps,
distanceToNextManeuver = 42.0,
initExpanded = true)
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import androidx.lifecycle.viewModelScope
import com.stadiamaps.ferrostar.core.extensions.currentRoadName
import com.stadiamaps.ferrostar.core.extensions.deviation
import com.stadiamaps.ferrostar.core.extensions.progress
import com.stadiamaps.ferrostar.core.extensions.remainingSteps
import com.stadiamaps.ferrostar.core.extensions.visualInstruction
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import uniffi.ferrostar.GeographicCoordinate
import uniffi.ferrostar.RouteDeviation
import uniffi.ferrostar.RouteStep
import uniffi.ferrostar.SpokenInstruction
import uniffi.ferrostar.TripProgress
import uniffi.ferrostar.TripState
Expand Down Expand Up @@ -49,7 +51,9 @@ data class NavigationUiState(
/** If true, spoken instructions will not be synthesized. */
val isMuted: Boolean?,
/** The name of the road which the current route step is traversing. */
val currentStepRoadName: String?
val currentStepRoadName: String?,
/** The remaining steps in the trip (including the current step). */
val remainingSteps: List<RouteStep>?
) {
companion object {
fun fromFerrostar(
Expand All @@ -70,7 +74,8 @@ data class NavigationUiState(
isCalculatingNewRoute = coreState.isCalculatingNewRoute,
routeDeviation = coreState.tripState.deviation(),
isMuted = isMuted,
currentStepRoadName = coreState.tripState.currentRoadName())
currentStepRoadName = coreState.tripState.currentRoadName(),
remainingSteps = coreState.tripState.remainingSteps())
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,15 @@ fun TripState.currentRoadName() =
is TripState.Complete,
TripState.Idle -> null
}

/**
* Get the remaining steps (including the current) in the current trip.
*
* @return The list of remaining steps (if any).
*/
fun TripState.remainingSteps() =
when (this) {
is TripState.Navigating -> this.remainingSteps
is TripState.Complete,
TripState.Idle -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ class DemoNavigationViewModel : ViewModel(), NavigationViewModel {
.map { userLocation ->
// TODO: Heading
NavigationUiState(
userLocation, null, null, null, null, null, null, false, null, null, null)
userLocation, null, null, null, null, null, null, false, null, null, null, null)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
// TODO: Heading
initialValue =
NavigationUiState(
null, null, null, null, null, null, null, false, null, null, null))
null, null, null, null, null, null, null, false, null, null, null, null))

override fun toggleMute() {
// Do nothing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ fun LandscapeNavigationOverlayView(
Column(modifier = Modifier.fillMaxHeight().fillMaxWidth(0.5f)) {
uiState.visualInstruction?.let { instructions ->
InstructionsView(
instructions, distanceToNextManeuver = uiState.progress?.distanceToNextManeuver)
instructions,
remainingSteps = uiState.remainingSteps,
distanceToNextManeuver = uiState.progress?.distanceToNextManeuver)
}

Spacer(modifier = Modifier.weight(1f))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ fun PortraitNavigationOverlayView(
Column(modifier) {
uiState.visualInstruction?.let { instructions ->
InstructionsView(
instructions, distanceToNextManeuver = uiState.progress?.distanceToNextManeuver)
instructions,
remainingSteps = uiState.remainingSteps,
distanceToNextManeuver = uiState.progress?.distanceToNextManeuver)
}

val cameraIsTrackingLocation = camera.value.state is CameraState.TrackingUserLocationWithBearing
Expand Down
Loading