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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
@@ -50,6 +47,8 @@ import kotlin.math.roundToInt
/** /**
* Draggable dual-station elevation curve chart. * 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 @Composable
fun ElevationCurveChart( fun ElevationCurveChart(
@@ -57,6 +56,8 @@ fun ElevationCurveChart(
startTime: Long, startTime: Long,
endTime: Long, endTime: Long,
maxElev: Double, maxElev: Double,
progress: Float = 0.5f,
onProgressChange: (Float) -> Unit = {},
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
if (samples.isEmpty()) return if (samples.isEmpty()) return
@@ -73,8 +74,6 @@ fun ElevationCurveChart(
val gridColorArgb = gridColor.toArgb() val gridColorArgb = gridColor.toArgb()
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) } val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
var dragProgress by remember { mutableFloatStateOf(0.5f) }
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = modifier.fillMaxWidth()) {
Canvas( Canvas(
modifier = Modifier modifier = Modifier
@@ -83,12 +82,12 @@ fun ElevationCurveChart(
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
.pointerInput(Unit) { .pointerInput(Unit) {
detectTapGestures { offset -> detectTapGestures { offset ->
dragProgress = (offset.x / size.width.toFloat()).coerceIn(0f, 1f) onProgressChange((offset.x / size.width.toFloat()).coerceIn(0f, 1f))
} }
} }
.pointerInput(Unit) { .pointerInput(Unit) {
detectDragGestures { _, dragAmount -> detectDragGestures { change, _ ->
dragProgress = (dragProgress + dragAmount.x / size.width.toFloat()).coerceIn(0f, 1f) onProgressChange((change.position.x / size.width.toFloat()).coerceIn(0f, 1f))
} }
} }
) { ) {
@@ -180,8 +179,8 @@ fun ElevationCurveChart(
drawPath(pathB, colorB, style = Stroke(width = 2.5f)) drawPath(pathB, colorB, style = Stroke(width = 2.5f))
// Drag indicator // Drag indicator
val dragX = plotLeft + dragProgress * plotWidth val dragX = plotLeft + progress * plotWidth
val dragTime = startTime + (dragProgress * (endTime - startTime)).toLong() val dragTime = startTime + (progress * (endTime - startTime)).toLong()
drawLine( drawLine(
onSurfaceColor, onSurfaceColor,
@@ -18,6 +18,8 @@
package com.rtbishop.look4sat.feature.mutual package com.rtbishop.look4sat.feature.mutual
import androidx.compose.foundation.Canvas 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.PathEffect
import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke 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.TextMeasurer
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText 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 * Polar radar chart showing the satellite track as seen from both stations
* on one plot. Station A is drawn solid, station B dashed. * 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 @Composable
fun MutualRadarView( fun MutualRadarView(
trackSamples: List<TrackSample>, trackSamples: List<TrackSample>,
progress: Float = 0.5f,
onProgressChange: (Float) -> Unit = {},
labelA: String = "站点A", labelA: String = "站点A",
labelB: String = "站点B", labelB: String = "站点B",
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -66,12 +73,29 @@ fun MutualRadarView(
val textColor = MaterialTheme.colorScheme.onSurfaceVariant val textColor = MaterialTheme.colorScheme.onSurfaceVariant
val measurer = rememberTextMeasurer() 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()) { Column(modifier = modifier.fillMaxWidth()) {
Canvas( Canvas(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1f) .aspectRatio(1f)
.padding(4.dp) .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 radius = size.minDimension / 2f * 0.92f
val center = Offset(size.width / 2f, size.height / 2f) 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.elevationA > 0f) drawCircle(colorA, 5f, losA)
if (last.elevationB > 0f) drawCircle(colorB, 5f, losB) 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 // Center dot
drawCircle(gridColor, 4f, center) drawCircle(gridColor, 4f, center)
} }
@@ -48,7 +48,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -361,6 +363,9 @@ private fun MutualPassCard(
onClick: () -> Unit, onClick: () -> Unit,
onNavigateToRadar: () -> 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( ElevatedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -419,6 +424,8 @@ private fun MutualPassCard(
) )
MutualRadarView( MutualRadarView(
trackSamples = pass.trackSamples, trackSamples = pass.trackSamples,
progress = dragProgress,
onProgressChange = { dragProgress = it },
labelA = "你", labelA = "你",
labelB = "友台" labelB = "友台"
) )
@@ -429,7 +436,9 @@ private fun MutualPassCard(
samples = pass.elevationSamples, samples = pass.elevationSamples,
startTime = pass.startTime, startTime = pass.startTime,
endTime = pass.endTime, 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)) Spacer(Modifier.height(8.dp))