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
+62 -42

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
**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
- **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
Zielposition verfolgt. Ein `launch` pro Fingerbewegung erzeugte bei 2000 Titeln hunderte
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
*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
+2 -2
View File
@@ -14,8 +14,8 @@ android {
applicationId = "com.novasonic.app"
minSdk = 26
targetSdk = 35
versionCode = 22
versionName = "1.19"
versionCode = 23
versionName = "1.20"
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
// 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.
* 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
fun Rollbalken(
state: LazyListState,
modifier: Modifier = Modifier,
breite: Dp = 6.dp,
griffBreite: Dp = 28.dp,
griffBreite: Dp = 24.dp,
minHoehe: Dp = 44.dp,
farbe: Color = Color.White,
) {
@@ -90,7 +96,7 @@ fun Rollbalken(
val anteil = if (ziehen) zielPos / maxIndex
else state.firstVisibleItemIndex.toFloat() / maxIndex
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.
val geo = rememberUpdatedState(Geometrie(maxIndex.toFloat(), frei, daumenY, daumenPx, spielPx))
@@ -106,47 +112,55 @@ fun Rollbalken(
.width(griffBreite)
.fillMaxHeight()
.onSizeChanged { spurPx = it.height.toFloat() }
.pointerInput(Unit) {
awaitEachGesture {
val start = awaitFirstDown(requireUnconsumed = false)
val g = geo.value
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()
zielPos = state.firstVisibleItemIndex.toFloat()
ziehen = true
try {
while (true) {
val ereignis = awaitPointerEvent()
val punkt = ereignis.changes.firstOrNull { it.id == start.id } ?: break
if (!punkt.pressed) break
val delta = punkt.positionChange().y
punkt.consume()
if (delta != 0f) {
val jetzt = geo.value
if (jetzt.frei > 0f) {
zielPos = (zielPos + delta * jetzt.maxIndex / jetzt.frei)
.coerceIn(0f, jetzt.maxIndex)
}
}
}
} finally {
ziehen = false
}
}
}
) {
val luftDp = with(dichte) { spielPx.toDp() }
Box(
Modifier
.align(Alignment.TopEnd)
.offset { IntOffset(0, daumenY.roundToInt()) }
.padding(end = 2.dp)
.width(breite)
.height(with(dichte) { daumenPx.toDp() })
.clip(RoundedCornerShape(50))
.background(farbe.copy(alpha = alpha))
)
// 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) {
awaitEachGesture {
val start = awaitFirstDown(requireUnconsumed = false)
if (geo.value.frei <= 0f) return@awaitEachGesture
start.consume()
zielPos = state.firstVisibleItemIndex.toFloat()
ziehen = true
try {
while (true) {
val ereignis = awaitPointerEvent()
val punkt = ereignis.changes.firstOrNull { it.id == start.id } ?: break
if (!punkt.pressed) break
val delta = punkt.positionChange().y
punkt.consume()
if (delta != 0f) {
val jetzt = geo.value
if (jetzt.frei > 0f) {
zielPos = (zielPos + delta * jetzt.maxIndex / jetzt.frei)
.coerceIn(0f, jetzt.maxIndex)
}
}
}
} finally {
ziehen = false
}
}
}
.padding(vertical = luftDp, horizontal = 0.dp),
contentAlignment = Alignment.CenterEnd,
) {
Box(
Modifier
.padding(end = 2.dp)
.width(breite)
.fillMaxHeight()
.clip(RoundedCornerShape(50))
.background(farbe.copy(alpha = alpha))
)
}
}
}