feat: bidirectional drag sync between elevation curve and radar track

- ElevationCurveChart is now controlled (progress + onProgressChange)
- MutualRadarView shows shared time-cursor positions for both stations
  and is draggable/tappable to move the cursor
- MutualPassCard owns a single dragProgress feeding both charts, so
  dragging either chart moves the other in sync
This commit is contained in:
atsunatsu committed 2026-08-03 00:58:09 +08:00
1 parent 4ca33c37ad
commit 28fa373db0
3 files changed
+51 -11

No files matched your search

@@ -30,10 +30,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.toArgb
@@ -50,6 +47,8 @@ import kotlin.math.roundToInt
/**
* Draggable dual-station elevation curve chart.
* Controlled component: [progress] (0..1) and [onProgressChange] are owned by the parent,
* so the same time cursor can be shared with the radar track view.
*/
@Composable
fun ElevationCurveChart(
@@ -57,6 +56,8 @@ fun ElevationCurveChart(
startTime: Long,
endTime: Long,
maxElev: Double,
progress: Float = 0.5f,
onProgressChange: (Float) -> Unit = {},
modifier: Modifier = Modifier
) {
if (samples.isEmpty()) return
@@ -73,8 +74,6 @@ fun ElevationCurveChart(
val gridColorArgb = gridColor.toArgb()
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
var dragProgress by remember { mutableFloatStateOf(0.5f) }
Column(modifier = modifier.fillMaxWidth()) {
Canvas(
modifier = Modifier
@@ -83,12 +82,12 @@ fun ElevationCurveChart(
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
.pointerInput(Unit) {
detectTapGestures { offset ->
dragProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f)
onProgressChange((offset.x / size.width.toFloat()).coerceIn(0f, 1f))
}
}
.pointerInput(Unit) {
detectDragGestures { _, dragAmount ->
dragProgress = (dragProgress + dragAmount.x / size.width.toFloat()).coerceIn(0f, 1f)
detectDragGestures { change, _ ->
onProgressChange((change.position.x / size.width.toFloat()).coerceIn(0f, 1f))
}
}
) {
@@ -180,8 +179,8 @@ fun ElevationCurveChart(
drawPath(pathB, colorB, style = Stroke(width = 2.5f))
// Drag indicator
val dragX = plotLeft + dragProgress * plotWidth
val dragTime = startTime + (dragProgress * (endTime - startTime)).toLong()
val dragX = plotLeft + progress * plotWidth
val dragTime = startTime + (progress * (endTime - startTime)).toLong()
drawLine(
onSurfaceColor,
@@ -18,6 +18,8 @@
package com.rtbishop.look4sat.feature.mutual
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -34,6 +36,7 @@ import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
@@ -50,10 +53,14 @@ private const val STROKE_WIDTH = 2.5f
/**
* Polar radar chart showing the satellite track as seen from both stations
* on one plot. Station A is drawn solid, station B dashed.
* Controlled: [progress] (0..1) and [onProgressChange] are owned by the parent
* so the time cursor can be shared with the elevation curve chart.
*/
@Composable
fun MutualRadarView(
trackSamples: List<TrackSample>,
progress: Float = 0.5f,
onProgressChange: (Float) -> Unit = {},
labelA: String = "站点A",
labelB: String = "站点B",
modifier: Modifier = Modifier
@@ -66,12 +73,29 @@ fun MutualRadarView(
val textColor = MaterialTheme.colorScheme.onSurfaceVariant
val measurer = rememberTextMeasurer()
// Current-time samples (both stations' position at the shared cursor)
val t0 = trackSamples.first().time
val t1 = trackSamples.last().time
val selectedTime = t0 + ((t1 - t0) * progress).toLong()
val visibleSamples = trackSamples.filter { it.time <= selectedTime }
val currentSample = visibleSamples.lastOrNull() ?: trackSamples.first()
Column(modifier = modifier.fillMaxWidth()) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.padding(4.dp)
.pointerInput(Unit) {
detectTapGestures { offset ->
onProgressChange((offset.x / size.width.toFloat()).coerceIn(0f, 1f))
}
}
.pointerInput(Unit) {
detectDragGestures { change, _ ->
onProgressChange((change.position.x / size.width.toFloat()).coerceIn(0f, 1f))
}
}
) {
val radius = size.minDimension / 2f * 0.92f
val center = Offset(size.width / 2f, size.height / 2f)
@@ -125,6 +149,14 @@ fun MutualRadarView(
if (last.elevationA > 0f) drawCircle(colorA, 5f, losA)
if (last.elevationB > 0f) drawCircle(colorB, 5f, losB)
// Shared time-cursor positions (both stations at selectedTime)
val cursorA = sph2Cart(center, currentSample.azimuthA, currentSample.elevationA, radius)
val cursorB = sph2Cart(center, currentSample.azimuthB, currentSample.elevationB, radius)
drawCircle(colorA, 14f, cursorA, style = Stroke(3f))
drawCircle(colorA, 6f, cursorA)
drawCircle(colorB, 14f, cursorB, style = Stroke(3f))
drawCircle(colorB, 6f, cursorB)
// Center dot
drawCircle(gridColor, 4f, center)
}
@@ -48,7 +48,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -361,6 +363,9 @@ private fun MutualPassCard(
onClick: () -> Unit,
onNavigateToRadar: () -> Unit
) {
// Shared time cursor: both the elevation curve and the radar track view
// are controlled by this single progress value for bidirectional drag sync.
var dragProgress by remember(pass) { mutableFloatStateOf(0.5f) }
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
@@ -419,6 +424,8 @@ private fun MutualPassCard(
)
MutualRadarView(
trackSamples = pass.trackSamples,
progress = dragProgress,
onProgressChange = { dragProgress = it },
labelA = "你",
labelB = "友台"
)
@@ -429,7 +436,9 @@ private fun MutualPassCard(
samples = pass.elevationSamples,
startTime = pass.startTime,
endTime = pass.endTime,
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0)
maxElev = maxOf(pass.maxElevationA, pass.maxElevationB, 10.0),
progress = dragProgress,
onProgressChange = { dragProgress = it }
)
}
Spacer(Modifier.height(8.dp))