Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 39 additions & 35 deletions app/src/main/java/com/matedroid/ui/components/DualAxisLineChart.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand Down Expand Up @@ -176,6 +175,32 @@ fun DualAxisLineChart(
.fillMaxWidth()
.height(totalHeightDp)
.onSizeChanged { canvasWidthPx = it.width.toFloat() }
// Tap toggles the tooltip at the nearest point (ignoring the time-label strip below the chart).
.pointerInput(chartDataLeft, chartDataRight) {
if (dataSize < 2) return@pointerInput
detectTapGestures { offset ->
if (offset.y > chartHeightPx) return@detectTapGestures
val cWidth = size.width.toFloat() - rightLabelWidth
val stepX = cWidth / (dataSize - 1).coerceAtLeast(1)
val index = (offset.x / stepX).roundToInt().coerceIn(0, dataSize - 1)
if (selectedPoint?.index == index) {
selectedPoint = null
onXSelected?.invoke(null)
} else {
val fraction = if (dataSize > 1) index.toFloat() / (dataSize - 1) else 0f
val pointX = index * stepX
val leftVal = valueAtFraction(chartDataLeft.displayPoints, fraction)
val rightVal = valueAtFraction(chartDataRight.displayPoints, fraction)
val leftY = if (leftVal != null) {
chartHeightPx * (1 - (leftVal - chartDataLeft.minValue) / chartDataLeft.range)
} else offset.y
selectedPoint = DualSelectedPoint(index, leftVal, rightVal, Offset(pointX, leftY))
onXSelected?.invoke(fraction)
}
}
}
// Only claim HORIZONTAL drags (scrubbing the crosshair); vertical swipes fall
// through to the enclosing scroll container so the page still scrolls.
.pointerInput(chartDataLeft, chartDataRight) {
if (dataSize < 2) return@pointerInput

Expand All @@ -200,38 +225,17 @@ fun DualAxisLineChart(
onXSelected?.invoke(fraction)
}

awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
down.consume()
isUserInteracting = true

if (down.position.y > chartHeightPx) {
isUserInteracting = false
return@awaitEachGesture
}

val width = size.width.toFloat()
val cWidth = width - rightLabelWidth
val stepX = cWidth / (dataSize - 1).coerceAtLeast(1)
val initialIndex = ((down.position.x / stepX).roundToInt()).coerceIn(0, dataSize - 1)
val wasSelectedAtSameIndex = selectedPoint?.index == initialIndex

updateSelection(down.position.x, down.position.y)

var hasDragged = false
drag(down.id) { change ->
change.consume()
hasDragged = true
updateSelection(change.position.x, change.position.y)
}

if (!hasDragged && wasSelectedAtSameIndex) {
selectedPoint = null
onXSelected?.invoke(null)
}

isUserInteracting = false
}
detectHorizontalDragGestures(
onDragStart = { offset ->
if (offset.y <= chartHeightPx) {
isUserInteracting = true
updateSelection(offset.x, offset.y)
}
},
onDragEnd = { isUserInteracting = false },
onDragCancel = { isUserInteracting = false },
onHorizontalDrag = { change, _ -> updateSelection(change.position.x, change.position.y) }
)
}
) {
val width = size.width
Expand Down
59 changes: 30 additions & 29 deletions app/src/main/java/com/matedroid/ui/components/OptimizedLineChart.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
Expand Down Expand Up @@ -142,6 +141,28 @@ fun OptimizedLineChart(
.fillMaxWidth()
.height(totalHeightDp)
.onSizeChanged { canvasWidthPx = it.width.toFloat() }
// Tap toggles the tooltip at the nearest point.
.pointerInput(chartData) {
val points = chartData.displayPoints
if (points.isEmpty()) return@pointerInput
detectTapGestures { offset ->
val width = size.width.toFloat()
val stepX = width / (points.size - 1).coerceAtLeast(1)
val index = (offset.x / stepX).roundToInt().coerceIn(0, points.lastIndex)
if (selectedPoint?.index == index) {
selectedPoint = null
onXSelected?.invoke(null)
} else {
val fraction = if (points.size > 1) index.toFloat() / (points.size - 1) else 0f
val pointX = index * stepX
val pointY = chartHeightPx * (1 - (points[index] - chartData.minValue) / chartData.range)
selectedPoint = SelectedPoint(index, points[index], Offset(pointX, pointY))
onXSelected?.invoke(fraction)
}
}
}
// Only claim HORIZONTAL drags (scrubbing the crosshair); vertical swipes fall
// through to the enclosing scroll container so the page still scrolls.
.pointerInput(chartData) {
val points = chartData.displayPoints
if (points.isEmpty()) return@pointerInput
Expand All @@ -157,32 +178,12 @@ fun OptimizedLineChart(
onXSelected?.invoke(fraction)
}

awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
down.consume()
isUserInteracting = true

val width = size.width.toFloat()
val stepX = width / (points.size - 1).coerceAtLeast(1)
val initialIndex = ((down.position.x / stepX).roundToInt()).coerceIn(0, points.lastIndex)
val wasSelectedAtSameIndex = selectedPoint?.index == initialIndex

updateSelection(down.position.x)

var hasDragged = false
drag(down.id) { change ->
change.consume()
hasDragged = true
updateSelection(change.position.x)
}

if (!hasDragged && wasSelectedAtSameIndex) {
selectedPoint = null
onXSelected?.invoke(null)
}

isUserInteracting = false
}
detectHorizontalDragGestures(
onDragStart = { offset -> isUserInteracting = true; updateSelection(offset.x) },
onDragEnd = { isUserInteracting = false },
onDragCancel = { isUserInteracting = false },
onHorizontalDrag = { change, _ -> updateSelection(change.position.x) }
)
}
) {
val width = size.width
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package com.matedroid.ui.components

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand All @@ -25,6 +24,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.foundation.layout.offset
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand Down Expand Up @@ -95,6 +97,9 @@ fun PowerSocOverlayChart(
val topPad = with(density) { 6.dp.toPx() }

var selectedSoc by remember { mutableStateOf<Float?>(null) }
// Track the container + tooltip widths so the tooltip can follow the crosshair X (clamped on screen).
var containerWidthPx by remember { mutableStateOf(0) }
var tooltipWidthPx by remember { mutableStateOf(0) }

// Parent bumps dismissKey (on an outside tap or a scroll) to clear the tooltip.
LaunchedEffect(dismissKey) { selectedSoc = null }
Expand All @@ -107,26 +112,29 @@ fun PowerSocOverlayChart(
}
}

Box(modifier = modifier.fillMaxWidth()) {
Box(modifier = modifier
.fillMaxWidth()
.onSizeChanged { containerWidthPx = it.width }
) {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(chartHeight + 18.dp)
// Tap sets the crosshair at that SoC.
.pointerInput(valid, socMin, socMax) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
down.consume()
fun update(x: Float) {
val frac = (x / size.width).coerceIn(0f, 1f)
selectedSoc = socMin + frac * socRange
}
update(down.position.x)
drag(down.id) { change ->
change.consume()
update(change.position.x)
}
detectTapGestures { offset ->
selectedSoc = socMin + (offset.x / size.width).coerceIn(0f, 1f) * socRange
}
}
// Only claim HORIZONTAL drags (scrubbing); vertical swipes fall through to the
// enclosing scroll container so the page still scrolls.
.pointerInput(valid, socMin, socMax) {
detectHorizontalDragGestures(
onHorizontalDrag = { change, _ ->
selectedSoc = socMin + (change.position.x / size.width).coerceIn(0f, 1f) * socRange
}
)
}
) {
val w = size.width
val plotH = chartHeightPx - topPad
Expand Down Expand Up @@ -206,24 +214,35 @@ fun PowerSocOverlayChart(
}
}

// Tooltip — SoC and each session's power at the crosshair
// Tooltip — SoC and each session's power at the crosshair. Tracks the crosshair X
// (clamped on screen) and uses the same inverse-surface style as the other chart tooltips.
selectedSoc?.let { soc ->
val rows = renderCurves.mapNotNull { c -> interpolatePower(c.points, soc)?.let { c to it } }
if (rows.isNotEmpty()) {
val tooltipBg = MaterialTheme.colorScheme.inverseSurface
val tooltipFg = MaterialTheme.colorScheme.inverseOnSurface
Column(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 4.dp)
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceVariant)
.align(Alignment.TopStart)
.offset {
val crosshairX = if (containerWidthPx > 0) {
((soc - socMin) / socRange) * containerWidthPx
} else 0f
val maxX = (containerWidthPx - tooltipWidthPx).coerceAtLeast(0)
val x = (crosshairX - tooltipWidthPx / 2f).coerceIn(0f, maxX.toFloat())
IntOffset(x.roundToInt(), 4.dp.roundToPx())
}
.onSizeChanged { tooltipWidthPx = it.width }
.clip(RoundedCornerShape(8.dp))
.background(tooltipBg)
.padding(horizontal = 10.dp, vertical = 6.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
text = "${soc.roundToInt()}$xUnit$xCaption",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
color = tooltipFg
)
rows.forEach { (curve, power) ->
Row(verticalAlignment = Alignment.CenterVertically) {
Expand All @@ -237,7 +256,7 @@ fun PowerSocOverlayChart(
Text(
text = "${power.roundToInt()} $valueUnit",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = tooltipFg
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.matedroid.ui.screens.charges

import android.content.Intent
import android.net.Uri
import android.view.MotionEvent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
Expand Down Expand Up @@ -819,19 +818,9 @@ private fun ChargeMapCard(latitude: Double, longitude: Double, accent: Color) {
MapView(ctx).apply {
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true)
// Tell the parent vertical scroll to stop intercepting
// touches so single-finger drag pans the map instead
// of scrolling the page.
// One finger scrolls the surrounding page; two fingers pan/zoom the map.
setOnTouchListener { v, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE,
MotionEvent.ACTION_POINTER_DOWN ->
v.parent?.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL ->
v.parent?.requestDisallowInterceptTouchEvent(false)
}
v.parent?.requestDisallowInterceptTouchEvent(event.pointerCount >= 2)
false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.matedroid.ui.screens.drives
import android.content.Intent
import android.graphics.Paint
import android.net.Uri
import android.view.MotionEvent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
Expand Down Expand Up @@ -738,16 +737,9 @@ private fun DriveMapCard(positions: List<DrivePosition>, routeColor: Color) {
MapView(ctx).apply {
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true)
// One finger scrolls the surrounding page; two fingers pan/zoom the map.
setOnTouchListener { v, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE,
MotionEvent.ACTION_POINTER_DOWN ->
v.parent?.requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL ->
v.parent?.requestDisallowInterceptTouchEvent(false)
}
v.parent?.requestDisallowInterceptTouchEvent(event.pointerCount >= 2)
false
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,11 @@ private fun CountryMapCard(
MapView(ctx).apply {
setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true)
// One finger scrolls the surrounding page; two fingers pan/zoom the map.
setOnTouchListener { v, event ->
v.parent?.requestDisallowInterceptTouchEvent(event.pointerCount >= 2)
false
}
}
},
update = { mapView ->
Expand Down
Loading
Loading