fix(android): Rollbalken verschluckte das Kontextmenue der Songzeilen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-22 20:45:20 +02:00
1 parent d5b5c8cf84
commit 2641a6df44
3 files changed
+32 -12

No files matched your search

+7 -1
View File
@@ -80,7 +80,7 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d
## [3.1] — 2026-09-22 ## [3.1] — 2026-09-22
**Android:** 1.15 (versionCode 18) → 1.19 (versionCode 22). 1.16 brachte den Rollbalken, 1.17 und 1.18 machten ihn bedienbar, 1.19 bringt das Bearbeiten der Warteschlange. **Android:** 1.15 (versionCode 18) → 1.20 (versionCode 23). 1.16 brachte den Rollbalken, 1.17 bis 1.20 machten ihn bedienbar, 1.19 bringt das Bearbeiten der Warteschlange.
### Added ### Added
- **Die Warteschlange lässt sich bearbeiten (Web + Android)** — bisher war sie eine reine Anzeige: - **Die Warteschlange lässt sich bearbeiten (Web + Android)** — bisher war sie eine reine Anzeige:
@@ -150,6 +150,12 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d
- Gescrollt wird außerdem nicht mehr im Zeigerereignis, sondern in **einem** Effekt, der die - Gescrollt wird außerdem nicht mehr im Zeigerereignis, sondern in **einem** Effekt, der die
Zielposition verfolgt. Ein `launch` pro Fingerbewegung erzeugte bei 2000 Titeln hunderte Zielposition verfolgt. Ein `launch` pro Fingerbewegung erzeugte bei 2000 Titeln hunderte
konkurrierende Scroll-Aufrufe, die sich gegenseitig überholten. konkurrierende Scroll-Aufrufe, die sich gegenseitig überholten.
- **Nachtrag 1.20: der Balken verschluckte das ⋮ der Songzeilen.** Compose reicht eine Berührung
nur an das **oberste** getroffene Element weiter — der Greifstreifen lag über die volle Höhe
am rechten Rand und nahm damit jeden Tipper auf das Kontextmenü entgegen, obwohl er ihn gar
nicht wollte. Jetzt hört nur noch der **Daumen** zu (mit 10 dp Luft darüber und darunter); der
Rahmen darum hat kein `pointerInput` und ist dadurch durchlässig. Der Streifen ist außerdem von
28 auf 24 dp geschrumpft.
- **„Neu von deinen Künstlern" sitzt jetzt in den Favoriten (Web)** — es stand links unter - **„Neu von deinen Künstlern" sitzt jetzt in den Favoriten (Web)** — es stand links unter
*Entdecken*, in Android dagegen als Knopf „✨ Neu" in den Favoriten. Zwei Plattformen, zwei Orte *Entdecken*, in Android dagegen als Knopf „✨ Neu" in den Favoriten. Zwei Plattformen, zwei Orte
für dieselbe Sache. Das Web folgt jetzt Android: der Eintrag in der Seitenleiste ist weg, in der für dieselbe Sache. Das Web folgt jetzt Android: der Eintrag in der Seitenleiste ist weg, in der
+2 -2
View File
@@ -14,8 +14,8 @@ android {
applicationId = "com.novasonic.app" applicationId = "com.novasonic.app"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 22 versionCode = 23
versionName = "1.19" versionName = "1.20"
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in // Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der // gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
// Platzhalter stehen und der App-Link greift schlicht nicht. // Platzhalter stehen und der App-Link greift schlicht nicht.
@@ -53,13 +53,19 @@ import kotlin.math.roundToInt
* *
* Gescrollt wird nicht im Zeigerereignis, sondern in einem eigenen Effekt, der [zielPos] verfolgt. * Gescrollt wird nicht im Zeigerereignis, sondern in einem eigenen Effekt, der [zielPos] verfolgt.
* Ein `launch` pro Fingerbewegung erzeugte bei 2000 Titeln hunderte konkurrierende Scroll-Aufrufe. * Ein `launch` pro Fingerbewegung erzeugte bei 2000 Titeln hunderte konkurrierende Scroll-Aufrufe.
*
* Und eine vierte, die erst nach dem Ausliefern auffiel: **nur der Daumen nimmt Beruehrungen an,
* nicht die ganze Spur.** Compose reicht eine Beruehrung nur an das oberste getroffene Element
* weiter — ein Streifen ueber die volle Hoehe verschluckte damit das ⋮ jeder Songzeile, obwohl er
* die Beruehrung gar nicht wollte. Der aeussere Rahmen hat deshalb KEIN `pointerInput` und ist
* dadurch durchlaessig; nur der Daumen selbst hoert zu, mit etwas Luft nach oben und unten.
*/ */
@Composable @Composable
fun Rollbalken( fun Rollbalken(
state: LazyListState, state: LazyListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
breite: Dp = 6.dp, breite: Dp = 6.dp,
griffBreite: Dp = 28.dp, griffBreite: Dp = 24.dp,
minHoehe: Dp = 44.dp, minHoehe: Dp = 44.dp,
farbe: Color = Color.White, farbe: Color = Color.White,
) { ) {
@@ -90,7 +96,7 @@ fun Rollbalken(
val anteil = if (ziehen) zielPos / maxIndex val anteil = if (ziehen) zielPos / maxIndex
else state.firstVisibleItemIndex.toFloat() / maxIndex else state.firstVisibleItemIndex.toFloat() / maxIndex
val daumenY = frei * anteil.coerceIn(0f, 1f) val daumenY = frei * anteil.coerceIn(0f, 1f)
val spielPx = with(dichte) { 24.dp.toPx() } val spielPx = with(dichte) { 10.dp.toPx() } // Luft ober- und unterhalb des Daumens
// Was die Geste zur Laufzeit braucht — nicht als pointerInput-Schluessel, siehe oben. // Was die Geste zur Laufzeit braucht — nicht als pointerInput-Schluessel, siehe oben.
val geo = rememberUpdatedState(Geometrie(maxIndex.toFloat(), frei, daumenY, daumenPx, spielPx)) val geo = rememberUpdatedState(Geometrie(maxIndex.toFloat(), frei, daumenY, daumenPx, spielPx))
@@ -106,13 +112,20 @@ fun Rollbalken(
.width(griffBreite) .width(griffBreite)
.fillMaxHeight() .fillMaxHeight()
.onSizeChanged { spurPx = it.height.toFloat() } .onSizeChanged { spurPx = it.height.toFloat() }
) {
val luftDp = with(dichte) { spielPx.toDp() }
Box(
Modifier
.align(Alignment.TopEnd)
// Der Greifbereich beginnt eine Spielbreite ueber dem Daumen und ist entsprechend
// hoeher; der sichtbare Balken sitzt durch das Innenmass wieder genau auf daumenY.
.offset { IntOffset(0, (daumenY - spielPx).roundToInt()) }
.width(griffBreite)
.height(with(dichte) { (daumenPx + 2 * spielPx).toDp() })
.pointerInput(Unit) { .pointerInput(Unit) {
awaitEachGesture { awaitEachGesture {
val start = awaitFirstDown(requireUnconsumed = false) val start = awaitFirstDown(requireUnconsumed = false)
val g = geo.value if (geo.value.frei <= 0f) return@awaitEachGesture
val aufDemDaumen = start.position.y >= g.daumenY - g.spiel &&
start.position.y <= g.daumenY + g.daumenPx + g.spiel
if (!aufDemDaumen || g.frei <= 0f) return@awaitEachGesture
start.consume() start.consume()
zielPos = state.firstVisibleItemIndex.toFloat() zielPos = state.firstVisibleItemIndex.toFloat()
ziehen = true ziehen = true
@@ -136,19 +149,20 @@ fun Rollbalken(
} }
} }
} }
.padding(vertical = luftDp, horizontal = 0.dp),
contentAlignment = Alignment.CenterEnd,
) { ) {
Box( Box(
Modifier Modifier
.align(Alignment.TopEnd)
.offset { IntOffset(0, daumenY.roundToInt()) }
.padding(end = 2.dp) .padding(end = 2.dp)
.width(breite) .width(breite)
.height(with(dichte) { daumenPx.toDp() }) .fillMaxHeight()
.clip(RoundedCornerShape(50)) .clip(RoundedCornerShape(50))
.background(farbe.copy(alpha = alpha)) .background(farbe.copy(alpha = alpha))
) )
} }
} }
}
/** Nur ein Buendel der Werte, die die Geste zur Laufzeit nachliest. */ /** Nur ein Buendel der Werte, die die Geste zur Laufzeit nachliest. */
private data class Geometrie( private data class Geometrie(