mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-10-02 16:44:33 -04:00
feat(map-maplibre): start with collapsed attribution strip on seconda… (#7424)
This commit is contained in:
1 parent
6279d660d3
commit
b12ca979c8
12 files changed
+155
-17
No files matched your search
@@ -35,11 +35,13 @@ fun NodeTrackMap(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedPositionTime: Int? = null,
|
||||
onPositionSelect: ((Int) -> Unit)? = null,
|
||||
showAttribution: Boolean = true,
|
||||
) = MapLibreNodeTrackMap(
|
||||
destNum = destNum,
|
||||
positions = positions,
|
||||
modifier = modifier,
|
||||
selectedPositionTime = selectedPositionTime,
|
||||
onPositionSelect = onPositionSelect,
|
||||
showAttribution = showAttribution,
|
||||
customBasemaps = { androidCustomRasterBasemaps() },
|
||||
)
|
||||
@@ -41,6 +41,8 @@ fun NodeTrackMap(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedPositionTime: Int? = null,
|
||||
onPositionSelect: ((Int) -> Unit)? = null,
|
||||
// Accepted for the shared seam and ignored: Google Maps draws its own attribution, in MapView.
|
||||
@Suppress("UNUSED_PARAMETER") showAttribution: Boolean = true,
|
||||
) {
|
||||
val vm = koinViewModel<NodeMapViewModel>()
|
||||
vm.setDestNum(destNum)
|
||||
|
||||
@@ -259,13 +259,14 @@ class MainActivity : AppCompatActivity() {
|
||||
LocalSitePlannerAvailable provides true,
|
||||
LocalInlineMapProvider provides { node, modifier -> InlineMap(node, modifier) },
|
||||
LocalNodeTrackMapProvider provides
|
||||
{ destNum, positions, modifier, selectedPositionTime, onPositionSelected ->
|
||||
{ destNum, positions, modifier, selectedPositionTime, onPositionSelected, showAttribution ->
|
||||
org.meshtastic.app.map.node.NodeTrackMap(
|
||||
destNum,
|
||||
positions,
|
||||
modifier,
|
||||
selectedPositionTime,
|
||||
onPositionSelected,
|
||||
showAttribution,
|
||||
)
|
||||
},
|
||||
LocalTracerouteMapOverlayInsetsProvider provides getTracerouteMapOverlayInsets(),
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ID>CompositionLocalAllowlist:LocalNfcScannerProvider.kt:val LocalNfcScannerSupported = compositionLocalOf { false }</ID>
|
||||
<ID>CompositionLocalAllowlist:LocalNfcScannerProvider.kt:val LocalNfcWriterProvider = compositionLocalOf<@Composable (url: String, onResult: (Boolean) -> Unit, onNfcDisabled: () -> Unit) -> Unit> { { _, _, _ -> } }</ID>
|
||||
<ID>CompositionLocalAllowlist:LocalNodeMapScreenProvider.kt:/** * Provides the platform-specific Map Screen for a Node (e.g. Google Maps or OSMDroid on Android). On Desktop or JVM * targets where native maps aren't available yet, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalNodeMapScreenProvider = compositionLocalOf<@Composable (destNum: Int, onNavigateUp: () -> Unit) -> Unit> { { destNum, _ -> PlaceholderScreen("Node Map ($destNum)") } }</ID>
|
||||
<ID>CompositionLocalAllowlist:LocalNodeTrackMapProvider.kt:/** * Provides an embeddable position-track map composable that renders a polyline with markers for the given [positions]. * Unlike [LocalNodeMapScreenProvider], this does **not** include a Scaffold or AppBar — it is designed to be embedded * inside another screen layout (e.g. the position-log adaptive layout). * * Supports optional synchronized selection: * - [selectedPositionTime]: the `Position.time` of the currently selected position (or `null` for no selection). When * non-null, the map should visually highlight the corresponding marker and center the camera on it. * - [onPositionSelected]: callback invoked when a position marker is tapped on the map, passing the `Position.time` so * the host can synchronize the card list. * * On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalNodeTrackMapProvider = compositionLocalOf< @Composable ( destNum: Int, positions: List<Position>, modifier: Modifier, selectedPositionTime: Int?, onPositionSelected: ((Int) -> Unit)?, ) -> Unit, > { { _, _, _, _, _ -> PlaceholderScreen("Position Track Map") } }</ID>
|
||||
<ID>CompositionLocalAllowlist:LocalNodeTrackMapProvider.kt:/** * Provides an embeddable position-track map composable that renders a polyline with markers for the given [positions]. * Unlike [LocalNodeMapScreenProvider], this does **not** include a Scaffold or AppBar — it is designed to be embedded * inside another screen layout (e.g. the position-log adaptive layout). * * Supports optional synchronized selection: * - [selectedPositionTime]: the `Position.time` of the currently selected position (or `null` for no selection). When * non-null, the map should visually highlight the corresponding marker and center the camera on it. * - [onPositionSelected]: callback invoked when a position marker is tapped on the map, passing the `Position.time` so * the host can synchronize the card list. * - [showAttribution]: whether the credit opens with the map or stays collapsed behind its own button. The embedded * instance collapses it; a full-screen one does not. * * On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalNodeTrackMapProvider = compositionLocalOf< @Composable ( destNum: Int, positions: List<Position>, modifier: Modifier, selectedPositionTime: Int?, onPositionSelected: ((Int) -> Unit)?, showAttribution: Boolean, ) -> Unit, > { { _, _, _, _, _, _ -> PlaceholderScreen("Position Track Map") } }</ID>
|
||||
<ID>CompositionLocalAllowlist:LocalTracerouteMapOverlayInsetsProvider.kt:val LocalTracerouteMapOverlayInsetsProvider = compositionLocalOf { TracerouteMapOverlayInsets() }</ID>
|
||||
<ID>CompositionLocalAllowlist:LocalTracerouteMapProvider.kt:/** * Provides an embeddable traceroute map composable that renders node markers and forward/return offset polylines for a * traceroute result. Unlike [LocalMapViewProvider], this does **not** include a Scaffold, AppBar, waypoints, location * tracking, custom tiles, or any main-map features — it is designed to be embedded inside `TracerouteMapScreen`'s * scaffold. * * On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen]. * * Parameters: * - `tracerouteOverlay`: The overlay with forward/return route node nums. * - `tracerouteNodePositions`: Map of node num to position snapshots for the route nodes. * - `onMappableCountChanged`: Callback with (shown, total) node counts. * - `modifier`: Compose modifier for the map. */ @Suppress("Wrapping") val LocalTracerouteMapProvider = compositionLocalOf< @Composable ( tracerouteOverlay: TracerouteOverlay?, tracerouteNodePositions: Map<Int, Position>, onMappableCountChanged: (Int, Int) -> Unit, modifier: Modifier, ) -> Unit, > { { _, _, _, _ -> PlaceholderScreen("Traceroute Map") } }</ID>
|
||||
<ID>CompositionLocalAllowlist:LocalTracerouteMapScreenProvider.kt:/** * Provides the platform-specific Traceroute Map Screen. On Desktop or JVM targets where native maps aren't available * yet, it falls back to a [PlaceholderScreen]. */ @Suppress("Wrapping") val LocalTracerouteMapScreenProvider = compositionLocalOf<@Composable (destNum: Int, requestId: Int, logUuid: String?, onNavigateUp: () -> Unit) -> Unit> { { _, _, _, _ -> PlaceholderScreen("Traceroute Map") } }</ID>
|
||||
|
||||
+4
-1
@@ -32,6 +32,8 @@ import org.meshtastic.proto.Position
|
||||
* non-null, the map should visually highlight the corresponding marker and center the camera on it.
|
||||
* - [onPositionSelected]: callback invoked when a position marker is tapped on the map, passing the `Position.time` so
|
||||
* the host can synchronize the card list.
|
||||
* - [showAttribution]: whether the credit opens with the map or stays collapsed behind its own button. The embedded
|
||||
* instance collapses it; a full-screen one does not.
|
||||
*
|
||||
* On Desktop/JVM targets where native maps are not yet available, it falls back to a [PlaceholderScreen].
|
||||
*/
|
||||
@@ -44,7 +46,8 @@ val LocalNodeTrackMapProvider =
|
||||
modifier: Modifier,
|
||||
selectedPositionTime: Int?,
|
||||
onPositionSelected: ((Int) -> Unit)?,
|
||||
showAttribution: Boolean,
|
||||
) -> Unit,
|
||||
> {
|
||||
{ _, _, _, _, _ -> PlaceholderScreen("Position Track Map") }
|
||||
{ _, _, _, _, _, _ -> PlaceholderScreen("Position Track Map") }
|
||||
}
|
||||
@@ -456,13 +456,14 @@ private fun ApplicationScope.MeshtasticWindow(
|
||||
},
|
||||
LocalInlineMapProvider provides { node, modifier -> MapLibreInlineMap(node, modifier) },
|
||||
LocalNodeTrackMapProvider provides
|
||||
{ destNum, positions, modifier, selectedPositionTime, onPositionSelect ->
|
||||
{ destNum, positions, modifier, selectedPositionTime, onPositionSelect, showAttribution ->
|
||||
MapLibreNodeTrackMap(
|
||||
destNum = destNum,
|
||||
positions = positions,
|
||||
modifier = modifier,
|
||||
selectedPositionTime = selectedPositionTime,
|
||||
onPositionSelect = onPositionSelect,
|
||||
showAttribution = showAttribution,
|
||||
)
|
||||
},
|
||||
LocalDiscoveryMapProvider provides
|
||||
|
||||
+4
-3
@@ -92,6 +92,7 @@ fun MapLibreNodeTrackMap(
|
||||
modifier: Modifier = Modifier,
|
||||
selectedPositionTime: Int? = null,
|
||||
onPositionSelect: ((Int) -> Unit)? = null,
|
||||
showAttribution: Boolean = true,
|
||||
customBasemaps: @Composable () -> List<Basemap.Raster> = { customRasterBasemaps() },
|
||||
) {
|
||||
// Oldest first, as the Google flavor sorts its own track. Everything downstream reads order as age: the gradient
|
||||
@@ -141,7 +142,7 @@ fun MapLibreNodeTrackMap(
|
||||
// The map and its toolbar stay up even when the filter empties the track — otherwise the control that emptied it
|
||||
// disappears along with the points, leaving no way back.
|
||||
Box(modifier = modifier) {
|
||||
SecondaryMapSurface(mapState = mapState, basemaps = basemaps)
|
||||
SecondaryMapSurface(mapState = mapState, basemaps = basemaps, showAttribution = showAttribution)
|
||||
SecondaryMapChrome(
|
||||
mapState = mapState,
|
||||
basemaps = basemaps,
|
||||
@@ -168,8 +169,8 @@ fun MapLibreNodeTrackMap(
|
||||
?.takeIf { selected -> points.any { it.second == selected } }
|
||||
?.let { selected -> positions.firstOrNull { it.time == selected } },
|
||||
displayUnits = displayUnits,
|
||||
// Clear of the logo and attribution along the bottom edge, which the styles are licensed on condition of
|
||||
// showing. See MeshMapOrnaments.
|
||||
// Clear of the wordmark and attribution button along the bottom edge, which every map shows.
|
||||
// See MeshMapOrnaments.
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomStart)
|
||||
.padding(start = CARD_INSET.dp, end = CARD_INSET.dp, bottom = ORNAMENT_CLEARANCE.dp),
|
||||
|
||||
+9
@@ -35,15 +35,18 @@ import org.maplibre.compose.map.MapState
|
||||
import org.maplibre.compose.map.MapUiOptions
|
||||
import org.maplibre.compose.map.MaplibreMap
|
||||
import org.maplibre.compose.map.rememberMapState
|
||||
import org.maplibre.compose.overlay.include
|
||||
import org.maplibre.spatialk.geojson.BoundingBox
|
||||
import org.meshtastic.feature.map.component.MapEngineUnavailable
|
||||
import org.meshtastic.feature.map.maplibre.component.BasemapSelection
|
||||
import org.meshtastic.feature.map.maplibre.component.CollapsedAttributionOrnaments
|
||||
import org.meshtastic.feature.map.maplibre.component.MapZoom
|
||||
import org.meshtastic.feature.map.maplibre.component.SecondaryMapControls
|
||||
import org.meshtastic.feature.map.maplibre.layers.RasterBasemapLayer
|
||||
import org.meshtastic.feature.map.maplibre.style.Basemap
|
||||
import org.meshtastic.feature.map.maplibre.style.toBaseStyle
|
||||
import org.meshtastic.feature.map.maplibre.style.zoomRange
|
||||
import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay
|
||||
|
||||
/**
|
||||
* The map state the maps outside the main one all share.
|
||||
@@ -83,6 +86,9 @@ internal fun rememberSecondaryMapState(
|
||||
*
|
||||
* [basemaps] must be the same selection that state was built with: it supplies the zoom range the camera is held to,
|
||||
* and a different value here would clamp the camera to a range the loaded style cannot serve.
|
||||
*
|
||||
* [showAttribution] picks between the credit open and the credit collapsed; both keep the wordmark and the button that
|
||||
* reveals it. See [CollapsedAttributionOrnaments].
|
||||
*/
|
||||
@Composable
|
||||
internal fun SecondaryMapSurface(
|
||||
@@ -91,6 +97,7 @@ internal fun SecondaryMapSurface(
|
||||
modifier: Modifier = Modifier.fillMaxSize(),
|
||||
interactions: MapInteractions = SecondaryMapInteractions,
|
||||
uiOptions: MapUiOptions = MapUiOptions.Standard,
|
||||
showAttribution: Boolean = true,
|
||||
) {
|
||||
// Same guard as MeshMap, and in the same place: the style content is never composed without a presentation, so
|
||||
// it stops here too. Like MeshMap's, it covers the view and not the state.
|
||||
@@ -105,6 +112,8 @@ internal fun SecondaryMapSurface(
|
||||
CameraConstraints(minZoom = zoomRange.start.toDouble(), maxZoom = zoomRange.endInclusive.toDouble()),
|
||||
interactions = interactions,
|
||||
uiOptions = uiOptions,
|
||||
// The library's default, spelled out so the collapsed branch stays a narrowing of it.
|
||||
overlay = { include(if (showAttribution) MaplibreOverlay.Default else CollapsedAttributionOrnaments) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+9
-4
@@ -130,7 +130,12 @@ fun MapLibreInlineMap(
|
||||
// The same control every other map uses, at the same size. A shrunken variant was tried and looked out of place
|
||||
// against the rest of the map chrome for the ~25% of height it saved.
|
||||
Box(modifier = modifier) {
|
||||
SecondaryMapSurface(mapState = mapState, basemaps = basemaps, uiOptions = InlineMapUiOptions)
|
||||
SecondaryMapSurface(
|
||||
mapState = mapState,
|
||||
basemaps = basemaps,
|
||||
uiOptions = InlineMapUiOptions,
|
||||
showAttribution = false,
|
||||
)
|
||||
|
||||
MapZoom(mapState = mapState, basemap = basemaps.current)
|
||||
}
|
||||
@@ -166,7 +171,7 @@ fun MapLibreTracerouteMap(
|
||||
FitBoundsOnceVisible(mapState = mapState, key = hops) { nodesBoundingBox(hops) }
|
||||
|
||||
Box(modifier = modifier) {
|
||||
SecondaryMapSurface(mapState = mapState, basemaps = basemaps)
|
||||
SecondaryMapSurface(mapState = mapState, basemaps = basemaps, showAttribution = false)
|
||||
SecondaryMapChrome(mapState = mapState, basemaps = basemaps)
|
||||
}
|
||||
}
|
||||
@@ -226,7 +231,7 @@ fun MapLibreDiscoveryMap(
|
||||
// snippet, so the tapped node's numbers go at the foot of the map.
|
||||
DiscoveryNodeCard(
|
||||
node = selectedNode,
|
||||
// Clear of the logo and attribution row, which the styles are licensed on condition of showing.
|
||||
// Clear of the wordmark and attribution button along the bottom edge, which every map shows.
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomStart)
|
||||
.padding(start = CARD_INSET.dp, end = CARD_INSET.dp, bottom = ORNAMENT_CLEARANCE.dp),
|
||||
@@ -442,7 +447,7 @@ internal val SecondaryMapFitPadding = PaddingValues(start = 48.dp, top = 64.dp,
|
||||
internal const val CARD_INSET = 8
|
||||
|
||||
/**
|
||||
* Height to leave for the map's logo and attribution row, which the styles are licensed on condition of showing.
|
||||
* Height to leave for the map's wordmark and attribution button, which every map shows.
|
||||
*
|
||||
* See [org.meshtastic.feature.map.maplibre.component.MeshMapOrnaments].
|
||||
*/
|
||||
|
||||
+112
-3
@@ -16,30 +16,56 @@
|
||||
*/
|
||||
package org.meshtastic.feature.map.maplibre.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import org.maplibre.compose.camera.CameraMoveReason
|
||||
import org.maplibre.compose.map.LocalMapState
|
||||
import org.maplibre.compose.overlay.AttributionDefaults
|
||||
import org.maplibre.compose.overlay.AttributionLinks
|
||||
import org.maplibre.compose.overlay.DisappearingScaleBar
|
||||
import org.maplibre.compose.overlay.LocalViewportInsets
|
||||
import org.maplibre.compose.overlay.MaplibreLogo
|
||||
import org.maplibre.compose.overlay.attributions
|
||||
import org.maplibre.compose.overlay.include
|
||||
import org.maplibre.compose.overlay.MapOverlay as MaplibreOverlay
|
||||
|
||||
/**
|
||||
* The map's own ornaments: a scale bar while zooming, and the logo and attribution along the bottom.
|
||||
*
|
||||
* This is `MapOverlay.Default` with its compass removed. The mesh map already has a compass in its toolbar — one that
|
||||
* also toggles heading-lock — and drawing the library's as well would put two compasses on screen. The Google flavor
|
||||
* This reproduces `MapOverlay.Default`, which is a scale bar plus `AttributionOnly`, and is spelled out rather than
|
||||
* `include`d because the library's inset helper is internal to it.
|
||||
*
|
||||
* The library's own compass and zoom pair are absent by choice rather than by removal: the mesh map already has a
|
||||
* compass in its toolbar — one that also toggles heading-lock — and draws its zoom pair as `MapZoom`. The Google flavor
|
||||
* makes the same call from the other direction with `compassEnabled = false`.
|
||||
*
|
||||
* The logo and attribution are deliberately kept: the styles this map serves are licensed on the condition that they
|
||||
* are shown. Do not replace this with `MapOverlay.None`.
|
||||
* are shown. Do not replace this with `MapOverlay.None`; the main map is where the credit is read, not hidden behind a
|
||||
* tap.
|
||||
*
|
||||
* Scale-bar units are left to the library, which picks them by region — the same locale-driven approach the rest of the
|
||||
* app takes through `localeUnitsProvider`.
|
||||
@@ -68,3 +94,86 @@ internal val MeshMapOrnaments: MaplibreOverlay = MaplibreOverlay {
|
||||
// The logo and the attribution button, in the places `MapOverlay.Default` puts them.
|
||||
include(MaplibreOverlay.AttributionOnly)
|
||||
}
|
||||
|
||||
/**
|
||||
* `MapOverlay.Default` with the credit collapsed, for a map too small to host the credit without it overflowing: the
|
||||
* scale bar, the wordmark and the button all stay, and the button is what reveals the text.
|
||||
*
|
||||
* Composed rather than reached for because the library cannot be asked for it — [CollapsedAttributionButton] says why —
|
||||
* and the insets are spelled out because its `DefaultControls` is internal to it.
|
||||
*/
|
||||
internal val CollapsedAttributionOrnaments: MaplibreOverlay = MaplibreOverlay {
|
||||
val mapState = checkNotNull(LocalMapState.current)
|
||||
val viewportInsets = LocalViewportInsets.current
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.padding(viewportInsets)
|
||||
.consumeWindowInsets(viewportInsets)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing)
|
||||
.padding(MaplibreOverlay.Spacing),
|
||||
) {
|
||||
DisappearingScaleBar(
|
||||
metersPerDp = { mapState.viewport?.metersPerDpAtTarget ?: 0.0 },
|
||||
zoom = { mapState.cameraPosition.zoom },
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
)
|
||||
MaplibreLogo(Modifier.align(Alignment.BottomStart))
|
||||
CollapsedAttributionButton(Modifier.align(Alignment.BottomEnd))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How much of the map's width the expanded credit may take, the rest going to the toggle. Under half, so the strip
|
||||
* still reads as a strip on a thumbnail rather than a banner, and the map beside it stays usable.
|
||||
*/
|
||||
private const val CREDIT_WIDTH_FRACTION = 0.55f
|
||||
|
||||
/**
|
||||
* The library's attribution button, starting collapsed.
|
||||
*
|
||||
* `ExpandingAttributionButton` holds its own `expanded` flag, initialised to `true` and not settable from outside, so
|
||||
* `collapsedStyle` only styles the branch it is already in and the credit still opens. Holding the flag here is what
|
||||
* makes it start shut; everything else — icon, label, links — still comes from the library.
|
||||
*/
|
||||
@Composable
|
||||
private fun CollapsedAttributionButton(modifier: Modifier = Modifier) {
|
||||
val mapState = checkNotNull(LocalMapState.current)
|
||||
val mapStyle = mapState.style
|
||||
// Derived, not remembered: a basemap arrives as a style URL, so its sources are still empty on the first
|
||||
// composition and a remembered list would never re-read. As in the library's own button.
|
||||
val attributions by remember(mapStyle) { derivedStateOf { mapStyle.attributions() } }
|
||||
if (attributions.isEmpty()) return
|
||||
|
||||
var expanded by remember(mapStyle) { mutableStateOf(false) }
|
||||
|
||||
// As the library's button: a gesture closes the credit, so it does not sit over a short map after a pan. Narrowed
|
||||
// to gestures, or the programmatic moves our own zoom controls make would fold it away mid-animation.
|
||||
LaunchedEffect(mapState) {
|
||||
snapshotFlow { mapState.isCameraMoving && mapState.cameraMoveReason == CameraMoveReason.GESTURE }
|
||||
.filter { it }
|
||||
.collect { expanded = false }
|
||||
}
|
||||
|
||||
BoxWithConstraints(modifier.clip(RoundedCornerShape(24.dp)).background(AttributionDefaults.ContainerColor)) {
|
||||
// Read out here, not inside AnimatedVisibility: that lambda's receiver hides this scope.
|
||||
val creditMaxWidth = maxWidth * CREDIT_WIDTH_FRACTION
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// The toggle is last: the container is trailing-aligned, so a leading toggle would sit inboard of the
|
||||
// credit
|
||||
// and drift inwards as it opens. A Row mirrors itself, so last is the trailing edge in RTL too.
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Box(
|
||||
Modifier
|
||||
// `AttributionLinks` scrolls its own single line, so it only needs a ceiling: unbounded it
|
||||
// measures at the credit's full width and sprawls past a short map.
|
||||
.widthIn(max = creditMaxWidth)
|
||||
.padding(start = 12.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
|
||||
) {
|
||||
AttributionLinks(attributions = attributions, textStyle = AttributionDefaults.ContentTextStyle)
|
||||
}
|
||||
}
|
||||
AttributionDefaults.button { expanded = !expanded }
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -37,6 +37,9 @@ private const val ZOOM_INSET = 8
|
||||
/**
|
||||
* Lift above the attribution button, which is 40dp of icon plus its pill padding and shares this corner. More than the
|
||||
* cards need on the other side, where only the 23dp wordmark sits.
|
||||
*
|
||||
* The button is there either way — the small maps only collapse its credit, see [CollapsedAttributionOrnaments] — so
|
||||
* this clears it whether it is expanded or not.
|
||||
*/
|
||||
private const val ZOOM_BOTTOM_INSET = 56
|
||||
|
||||
@@ -47,8 +50,8 @@ private const val ZOOM_BOTTOM_INSET = 56
|
||||
* extension because the placement is the point: a caller that had to align it itself would eventually align it
|
||||
* somewhere else.
|
||||
*
|
||||
* Lifted clear of the logo and attribution row along the bottom edge, which the styles are licensed on condition of
|
||||
* showing — see [MeshMapOrnaments].
|
||||
* Lifted clear of the wordmark and attribution button along the bottom edge, which every map shows. See
|
||||
* [MeshMapOrnaments] for why the main map carries the credit outright and the small maps behind the button.
|
||||
*/
|
||||
@Composable
|
||||
internal fun BoxScope.MapZoom(mapState: MapState, basemap: Basemap) {
|
||||
|
||||
+3
-1
@@ -81,7 +81,9 @@ fun PositionLogScreen(viewModel: MetricsViewModel, onNavigateUp: () -> Unit) {
|
||||
},
|
||||
chartPart = { modifier, selectedX, _, onPointSelected ->
|
||||
val selectedTime = selectedX?.toInt()
|
||||
trackMap(destNum, positions, modifier, selectedTime) { time -> onPointSelected(time.toDouble()) }
|
||||
// Positional: trackMap is a function type, so it takes no named arguments. Collapsed credit, this being a
|
||||
// strip inside a screen whose own map is a tab away.
|
||||
trackMap(destNum, positions, modifier, selectedTime, { time -> onPointSelected(time.toDouble()) }, false)
|
||||
},
|
||||
listPart = { modifier, selectedX, lazyListState, onCardClick ->
|
||||
LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) {
|
||||
|
||||
Reference in new issue
Block a user