fix(android): Now-Playing an Fold und Flip anpassen — Deckel, Hoehenmass, Rollbereich

Die Seite hatte keine Breitengrenze: das Cover war fillMaxWidth() mal aspectRatio(1f).
Aufgeklappt sind das ueber 800 dp, womit Titel, Fortschritt und Knoepfe aus dem Bild
geschoben wurden. Das Cover nimmt jetzt das Kleinste aus Breite, Hoehe abzueglich
Restbedarf und 400 dp; Titelblock/Fortschritt/Knoepfe sind auf 480 dp gedeckelt (wie
.np-inner im Web), Liedtext und Warteschlange auf 560 dp.

Auf kurzen Anzeigen (Flip-Frontdisplay, Querformat, kleine Handys) wurde die Knopfreihe
abgeschnitten. Jetzt schrumpfen dort Abstaende, Reiter und Abspielknopf, und die
Player-Seite laesst sich rollen, wenn es weiterhin nicht reicht: der Inhalt ist
mindestens so hoch wie die Seite, passt er, wird zentriert, passt er nicht, waechst er
darueber hinaus. Die Wischgeste zum Schliessen bleibt, weil ein Rollbereich ohne Weg die
Geste nicht verbraucht.

Entschieden wird nach Maszen, nicht nach Gerätenamen: schmal < 400 dp, kurz < 560 dp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-10-13 18:47:03 +02:00
1 parent b6e98463ee
commit 624832959d
3 files changed
+123 -26

No files matched your search

@@ -68,6 +68,27 @@ import androidx.compose.ui.input.pointer.positionChange
private enum class NpTab { PLAYER, LYRICS, QUEUE }
// ── Obergrenzen fuer grosse Anzeigen (Fold aufgeklappt, Tablet) ──────────────────────────────
// Bis 1.26 hatte diese Ansicht keine Breitengrenze: das Cover war `fillMaxWidth` mal
// `aspectRatio(1f)`. Auf einem Handy sind das knappe 360 dp und passt; auf dem inneren Display
// eines Fold sind es ueber 800 dp — ein quadratisches Cover dieser Groesse schob Titel,
// Fortschrittsbalken und Knoepfe aus dem Bild, und die ganze Seite wirkte ueberdimensional.
// Die Web-Ansicht deckelt dasselbe seit immer bei 480 px (`.np-inner { max-width: 480px }`).
// Zusaetzlich wird das Cover gegen die HOEHE gedeckelt: bei einem fast quadratischen Display ist
// die Breite kein gutes Mass mehr fuer ein quadratisches Bild.
private val NP_SPALTE_MAX = 480.dp // Player-Seite: Cover, Titel, Fortschritt, Knoepfe
private val NP_LISTE_MAX = 560.dp // Liedtext und Warteschlange — Listen duerfen etwas breiter
private val NP_COVER_MAX = 400.dp
// Was Titel, Album, Kuenstler, Fortschrittsbalken, Zeiten, Knopfreihe und die Abstaende
// dazwischen zusammen brauchen (gemessen an den gesetzten Groessen, mit etwas Luft). Das Cover
// bekommt den Rest — so passt die Seite auch auf kurze Anzeigen, wo bisher die Knopfreihe
// unten abgeschnitten wurde.
private val NP_RESTBEDARF = 260.dp
// Auf kurzen Anzeigen (Flip-Frontdisplay, Querformat) schrumpfen Abstaende und Abspielknopf
// zusaetzlich, der Rest braucht dann weniger — und wenn es trotzdem nicht reicht, laesst sich
// die Seite rollen.
private val NP_RESTBEDARF_KURZ = 200.dp
@Composable
fun NowPlayingScreen(
state: PlayerState,
@@ -167,11 +188,18 @@ fun NowPlayingScreen(
// Light scrim for readability
Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.15f)))
BoxWithConstraints(Modifier.fillMaxSize()) {
// Zwei Groessenfragen entscheiden das Layout, nicht ein Geraetename:
// schmal — passt die Kopfzeile mit drei Reitern noch in voller Groesse? (Flip-Deckel)
// kurz — bleibt nach Kopfzeile und Raendern ueberhaupt Platz? (Flip-Deckel, Querformat)
val schmal = maxWidth < 400.dp
val kurz = maxHeight < 560.dp
val obenPad = if (kurz) maxOf(statusBarTop + 4.dp, 20.dp) else topPad
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(top = topPad, bottom = maxOf(navBarBottom, 20.dp))
.padding(top = obenPad, bottom = maxOf(navBarBottom, if (kurz) 8.dp else 20.dp))
.pointerInput(Unit) {
var dragY = 0f
detectVerticalDragGestures(
@@ -185,9 +213,13 @@ fun NowPlayingScreen(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
.padding(horizontal = 20.dp)
.padding(bottom = 28.dp),
modifier = Modifier
// Ohne Grenze stehen Schliessen-Knopf und Menue auf einem breiten Display
// ganz aussen und die Reiter einsam in der Mitte.
.widthIn(max = NP_LISTE_MAX)
.fillMaxWidth()
.padding(horizontal = if (schmal) 8.dp else 20.dp)
.padding(bottom = if (kurz) 10.dp else 28.dp),
) {
NpCircleBtn(onClick = onDismiss) {
Icon(Icons.Default.KeyboardArrowDown, "Schließen",
@@ -209,7 +241,10 @@ fun NowPlayingScreen(
.clip(RoundedCornerShape(16.dp))
.background(if (sel) Color.White.copy(0.18f) else Color.Transparent)
.clickable { tab = t }
.padding(horizontal = 18.dp, vertical = 9.dp),
.padding(
horizontal = if (schmal) 9.dp else 18.dp,
vertical = if (kurz) 6.dp else 9.dp,
),
) {
Text(
when (t) {
@@ -217,7 +252,8 @@ fun NowPlayingScreen(
NpTab.LYRICS -> "Lyrics"
NpTab.QUEUE -> "Queue"
},
fontFamily = Outfit, fontWeight = FontWeight.Bold, fontSize = 13.sp,
fontFamily = Outfit, fontWeight = FontWeight.Bold,
fontSize = if (schmal) 11.sp else 13.sp,
color = if (sel) Color.White else Color.White.copy(alpha = 0.4f),
)
}
@@ -265,20 +301,43 @@ fun NowPlayingScreen(
) {
when (NpTab.entries[page]) {
// ── PLAYER TAB ─────────────────────────────────────────────────
NpTab.PLAYER -> {
NpTab.PLAYER -> BoxWithConstraints(Modifier.fillMaxSize()) {
var dragX by remember { mutableStateOf(0f) }
// Das Cover nimmt das KLEINSTE aus drei Maszen: verfuegbare Breite (minus der
// bisherigen 24 dp Rand je Seite), die Hoehe abzueglich dessen, was der Rest der
// Seite braucht, und eine feste Obergrenze. Auf einem hohen Handy gewinnt die
// Breite — dort bleibt alles wie bisher. Aufgeklappt greifen die anderen beiden.
val coverKante = minOf(
maxWidth - 48.dp,
(maxHeight - if (kurz) NP_RESTBEDARF_KURZ else NP_RESTBEDARF).coerceAtLeast(120.dp),
NP_COVER_MAX,
)
val spalte = minOf(maxWidth, NP_SPALTE_MAX)
// Vor dem Rollbereich festhalten: innerhalb des Box-Lambdas ist `maxHeight` des
// aeusseren Messrahmens nicht mehr erreichbar (Compose sperrt gleich markierte
// Layout-Bereiche gegeneinander ab).
val seitenHoehe = maxHeight
Spacer(Modifier.height(28.dp))
// Cover art — full content width, scale-animates like Apple Music
// Passt es, wird zentriert; passt es nicht, wird gescrollt. Beides mit einem
// Aufbau: der Inhalt ist mindestens so hoch wie die Seite (dann greift
// Arrangement.Center), darf aber darueber hinauswachsen — dann hat der
// Rollbereich etwas zu rollen. Auf dem Frontdisplay eines Flip bleibt sonst
// nichts uebrig, was ohne Abschneiden darstellbar waere.
// Die Wischgeste zum Schliessen bleibt erhalten: ein Rollbereich ohne Weg
// verbraucht die Geste nicht, sie erreicht also weiter die Seite darunter.
Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().heightIn(min = seitenHoehe),
) {
// Cover art — gedeckelt, scale-animates like Apple Music
AsyncImage(
model = coverUrl,
contentDescription = song?.title,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth()
.aspectRatio(1f)
.size(coverKante)
.graphicsLayer { scaleX = coverScale; scaleY = coverScale }
.shadow(40.dp, RoundedCornerShape(20.dp))
.clip(RoundedCornerShape(20.dp))
@@ -296,10 +355,10 @@ fun NowPlayingScreen(
},
)
Spacer(Modifier.height(24.dp))
Spacer(Modifier.height(if (kurz) 12.dp else 24.dp))
// ── Song info ──────────────────────────────────────────────
Column(Modifier.fillMaxWidth().padding(horizontal = 28.dp)) {
Column(Modifier.width(spalte).padding(horizontal = 28.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
@@ -388,10 +447,10 @@ fun NowPlayingScreen(
}
}
Spacer(Modifier.height(20.dp))
Spacer(Modifier.height(if (kurz) 10.dp else 20.dp))
// ── Seek bar ───────────────────────────────────────────────
Column(Modifier.fillMaxWidth().padding(horizontal = 24.dp)) {
Column(Modifier.width(spalte).padding(horizontal = 24.dp)) {
val progress = if (state.durationMs > 0)
(state.positionMs.toFloat() / state.durationMs).coerceIn(0f, 1f) else 0f
@@ -408,13 +467,13 @@ fun NowPlayingScreen(
}
}
Spacer(Modifier.height(12.dp))
Spacer(Modifier.height(if (kurz) 4.dp else 12.dp))
// ── Controls ───────────────────────────────────────────────
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
modifier = Modifier.width(spalte).padding(horizontal = 16.dp),
) {
NpCircleBtn(onClick = { controller.toggleShuffle() }, aktiv = state.shuffled) {
Icon(Icons.Default.Shuffle, null, modifier = Modifier.size(22.dp),
@@ -428,7 +487,7 @@ fun NowPlayingScreen(
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(68.dp)
.size(if (kurz) 56.dp else 68.dp)
.clip(CircleShape)
.background(Color.White)
.clickable { controller.togglePlay() },
@@ -452,7 +511,8 @@ fun NowPlayingScreen(
Icon(icon, null, modifier = Modifier.size(22.dp), tint = tint)
}
}
} // end Column (gedeckelte Player-Spalte)
} // end Box (Rollbereich, falls die Seite nicht reicht)
}
// ── LYRICS TAB ─────────────────────────────────────────────────
@@ -501,7 +561,8 @@ fun NowPlayingScreen(
if (lyricsState.lines.isNotEmpty()) {
Row(
modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp),
modifier = Modifier.widthIn(max = NP_LISTE_MAX)
.fillMaxWidth().padding(bottom = 4.dp),
horizontalArrangement = Arrangement.End,
) {
Row(
@@ -558,6 +619,8 @@ fun NowPlayingScreen(
state = lyricsListState,
modifier = Modifier
.weight(1f)
// Gedeckelt: eine Textzeile ueber 800 dp liest sich nicht mehr.
.widthIn(max = NP_LISTE_MAX)
.fillMaxWidth()
.onSizeChanged { lyricsViewH = it.height },
// Asymmetric padding so the first/last lines can still reach the anchor point.
@@ -578,7 +641,8 @@ fun NowPlayingScreen(
}
lyricsState.plainText.isNotBlank() -> LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth(),
modifier = Modifier.weight(1f)
.widthIn(max = NP_LISTE_MAX).fillMaxWidth(),
contentPadding = PaddingValues(vertical = 24.dp),
) {
item {
@@ -601,6 +665,7 @@ fun NowPlayingScreen(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.widthIn(max = NP_LISTE_MAX)
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 10.dp)
.clip(RoundedCornerShape(12.dp))
@@ -660,7 +725,8 @@ fun NowPlayingScreen(
var ziehOffset by remember { mutableFloatStateOf(0f) }
val queueLaenge = rememberUpdatedState(state.queue.size)
LazyColumn(
modifier = Modifier.weight(1f).fillMaxWidth(),
modifier = Modifier.weight(1f)
.widthIn(max = NP_LISTE_MAX).fillMaxWidth(),
contentPadding = PaddingValues(vertical = 4.dp),
) {
itemsIndexed(state.queue, key = { idx, _ -> idx }) { idx, qSong ->
@@ -764,6 +830,7 @@ fun NowPlayingScreen(
} // end inner Column
} // end HorizontalPager
}
} // end BoxWithConstraints (schmal/kurz)
}
}