-
Notifications
You must be signed in to change notification settings - Fork 21
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
Android route overview #345
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few comments from the discussion but have a good path forward.
...poseui/src/main/java/com/stadiamaps/ferrostar/composeui/config/VisualNavigationViewConfig.kt
Outdated
Show resolved
Hide resolved
.../src/main/java/com/stadiamaps/ferrostar/composeui/views/gridviews/NavigatingInnerGridView.kt
Outdated
Show resolved
Hide resolved
...ain/java/com/stadiamaps/ferrostar/maplibreui/views/overlays/PortraitNavigationOverlayView.kt
Outdated
Show resolved
Hide resolved
…l everywhere and can get more granular recompositions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be ready for review now! I've updated the video attached to the PR description showing the updated UX.
data class VisualNavigationViewConfig( | ||
var showMute: Boolean = false, | ||
var showZoom: Boolean = false, | ||
var buttonSize: DpSize = DpSize(56.dp, 56.dp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was previously an informal constant that was copied everywhere. This at least gives it a place to live, and opens the path for button scaling enhancements if needed.
} | ||
} | ||
|
||
// NOTE: Some controls hidden when the camera is not following the user |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me know if my memory of this is incorrect, but I believe your request on the call was to hide the mute button but leave zoom (if present), progress, and instructions.
@@ -79,6 +79,8 @@ data class NavigationUiState( | |||
currentStepRoadName = coreState.tripState.currentRoadName(), | |||
remainingSteps = coreState.tripState.remainingSteps()) | |||
} | |||
|
|||
fun isNavigating(): Boolean = progress != null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved this to the UI state class, since 1) it was always derived from this anyways, and 2) moving it here means we can pass only the UI state in some cases rather than a full view model.
onMapReadyCallback: ((Style) -> Unit)? = null, | ||
content: @Composable @MapLibreComposable ((NavigationUiState) -> Unit)? = null | ||
) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another notable change here and throughout: we don't actually need to be passing around State<T>
values directly and inspecting their values in most cases. Recomposition will already handle this at the relevant point, and this is a much cleaner API.
onMapReadyCallback = | ||
onMapReadyCallback ?: { if (isNavigating) camera.value = navigationCamera }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changed to be a nullable parameter rather than a capturing closure for two reasons.
- The old implementation relied on direct view model access, which we are no longer doing for good reasons.
- It avoids capturing state in parameters in ways that may be non-obvious. This is cleaner IMO and will still recompose properly off
uiState
when needed.
// Bottom padding must take the recenter button into account | ||
val bottomPadding = (progressViewHeight + this.buttonSize.height.value + 50) * scale |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let it be known that I really don't like that we have to do this as a result of moving the button, further reducing the amount of space available for the map. I still think the UI would make more sense if we left the recenter button in the top end grid cell.
import com.stadiamaps.ferrostar.maplibreui.NavigationViewMetrics | ||
|
||
@Composable | ||
fun VisualNavigationViewConfig.cameraControlState( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This is as good a place as any to drop this I think... I think an extension is better than a free function, but there's no place we colud put this that would remove a substantial number of parameters.
- Note that this can't live in the same file as the declaration in the
composeui
module, since almost everything about it is MapLibre specific.
@@ -36,9 +43,10 @@ import kotlinx.coroutines.flow.asStateFlow | |||
fun LandscapeNavigationOverlayView( | |||
modifier: Modifier, | |||
camera: MutableState<MapViewCamera>, | |||
navigationCamera: MapViewCamera = navigationMapViewCamera(), | |||
navigationCamera: MapViewCamera, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that making this a non-optional parameter was a net negative for code quality for not much benefit. It's way too easy to forget to pass it with this many layers, and in fact it was not actually passed down previously. I haven't changed the progressViewSize
parameter in this PR, but I also strongly suspect that the same is true of that as well and we should require it at compile itme.
@@ -74,11 +93,17 @@ fun LandscapeNavigationOverlayView( | |||
showMute = config.showMute, | |||
isMuted = uiState.isMuted, | |||
onClickMute = { viewModel.toggleMute() }, | |||
buttonSize = config.buttonSize, | |||
cameraControlState = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the (IMO fairly elegant) solution I ended up with. We can avoid 1) repeating a lot of code, and 2) leaking out the instruction view size (propagating more boilerplate) than if we moved it up a level (which was my first instinct. I'm pretty happy with this since it's minimal code, with all parameters readily available, and users of the overlay just get it "for free" (fewer composable parameters).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ended up discovering some bugs in real world testing, which led me to realize the view model swopping antipattern in the demo app HAD to go ;)
class DemoNavigationViewModel( | ||
// This is a simple example, but these would typically be dependency injected | ||
private val ferrostarCore: FerrostarCore = AppModule.ferrostarCore, | ||
) : ViewModel(), NavigationViewModel { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have revamped the view model so it is no longer crap.
viewModel.stopNavigation() | ||
val vm = DemoNavigationViewModel() | ||
viewModel = vm | ||
|
||
vm.startLocationUpdates(locationProvider) | ||
}, | ||
onTapExit = { viewModel.stopNavigation(stopLocationUpdates = false) }, | ||
userContent = { modifier -> | ||
if (!viewModel.isNavigating()) { | ||
if (!vmState.isNavigating()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These evil bits are gone!
fun stopNavigation(stopLocationUpdates: Boolean = true) { | ||
foregroundServiceManager?.stopService() | ||
locationProvider.removeListener(this) | ||
if (stopLocationUpdates) { | ||
locationProvider.removeListener(this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New argument that doesn't break source compatibility; lets us keep the location provider running, which makes life easier for single map view apps.
route: Route, | ||
config: NavigationControllerConfig? = null | ||
): DefaultNavigationViewModel { | ||
fun startNavigation(route: Route, config: NavigationControllerConfig? = null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Major change 🚨
The state flow was already exposed. And @Archdoog was telling me for some time that this was a bit of an antipattern 😅 So this resolves long-standing tech debt. We no longer return view models here, which actually encourages people to write their own to purpose if needed, and the "default" is still quite usable for modal activities.
trim.95AB815E-C784-4239-B898-120176CE11C8.MOV
Closes #158