fix(android): Rollbalken verschluckte das Kontextmenue der Songzeilen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d5b5c8cf84
commit
2641a6df44
3 files changed
+62
-42
No files matched your search
+7
-1
@@ -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
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user