Skip to content

Commit

Permalink
Upcoming instructions android (#326)
Browse files Browse the repository at this point in the history
* First pass at upcoming maneuvers view for Android

* Make it scroll

* Apply automatic changes

* Build the content view correctly

* Add snapshot tests

* Add s

* Improve the expand button UX

* Update snapshots

* Implement pill for expand/contract

* Regen snapshots

---------

Co-authored-by: ianthetechie <[email protected]>
  • Loading branch information
ianthetechie and ianthetechie authored Nov 1, 2024
1 parent 649a68c commit 35a9dee
Show file tree
Hide file tree
Showing 12 changed files with 193 additions and 19 deletions.
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
}
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)
}
}
}
}

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

0 comments on commit 35a9dee

Please sign in to comment.