Files
Android-247-Radio/docs/plans/2026-03-11-ticker-text-implementation.md

169 lines
5.9 KiB
Markdown
Raw Normal View History

# Ticker Text Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Replace the bounce-marquee animation with a constant-speed continuous ticker for long track titles.
**Architecture:** Replace the `BounceMarqueeText` composable with `TickerText` in `NowPlayingScreen.kt`. The new composable uses the same `rememberInfiniteTransition` + `animateFloat` pattern but with linear one-directional keyframes and duration derived from a fixed velocity (33 dp/s) rather than a clamped formula. One file changes.
**Tech Stack:** Jetpack Compose animation APIs (`rememberInfiniteTransition`, `animateFloat`, `keyframes`, `LinearEasing`)
---
### Task 1: Replace `BounceMarqueeText` with `TickerText`
**Files:**
- Modify: `app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt:449-543` (replace composable)
- Modify: `app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt:578` (update call site)
**Step 1: Replace the `BounceMarqueeText` composable (lines 449543) with `TickerText`**
The new composable:
```kotlin
@Composable
private fun TickerText(
text: String,
style: TextStyle,
color: Color,
strokeColor: Color? = null,
strokeWidth: Float = 2f,
velocityDpPerSecond: Float = 33f,
modifier: Modifier = Modifier
) {
val density = LocalDensity.current
val textMeasurer = rememberTextMeasurer()
val textWidthPx = remember(text, style) {
textMeasurer.measure(text, style, maxLines = 1, softWrap = false).size.width.toFloat()
}
var containerWidthPx by remember { mutableStateOf(0f) }
Box(
modifier = modifier
.fillMaxWidth()
.onSizeChanged { containerWidthPx = it.width.toFloat() }
.clipToBounds()
) {
val overflowPx = if (containerWidthPx > 0f)
(textWidthPx - containerWidthPx).coerceAtLeast(0f) else 0f
if (overflowPx > 0f) {
key(text) {
val velocityPxPerMs = with(density) { velocityDpPerSecond.dp.toPx() } / 1000f
val totalScrollPx = textWidthPx + containerWidthPx
val scrollMs = (totalScrollPx / velocityPxPerMs).toInt()
val initialDelayMs = 1500
val durationMs = initialDelayMs + scrollMs
val transition = rememberInfiniteTransition(label = "ticker")
val offset by transition.animateFloat(
initialValue = 0f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = durationMs
0f at 0 using LinearEasing
0f at initialDelayMs using LinearEasing
-(textWidthPx + containerWidthPx) at durationMs using LinearEasing
},
repeatMode = RepeatMode.Restart
),
label = "ticker"
)
Box(
modifier = Modifier
.wrapContentWidth(align = Alignment.Start, unbounded = true)
.align(Alignment.CenterStart)
.graphicsLayer { translationX = offset }
) {
if (strokeColor != null) {
Text(
text = text,
style = style.merge(TextStyle(drawStyle = Stroke(width = strokeWidth))),
color = strokeColor,
maxLines = 1,
softWrap = false
)
}
Text(
text = text,
style = style,
color = color,
maxLines = 1,
softWrap = false
)
}
}
} else {
Box(modifier = Modifier.align(Alignment.Center)) {
if (strokeColor != null) {
Text(
text = text,
style = style.merge(TextStyle(drawStyle = Stroke(width = strokeWidth))),
color = strokeColor,
maxLines = 1,
softWrap = false
)
}
Text(
text = text,
style = style,
color = color,
maxLines = 1,
softWrap = false
)
}
}
}
}
```
**Step 2: Update the call site in `TrackInfoSection` (line 578)**
Change:
```kotlin
BounceMarqueeText(
```
To:
```kotlin
TickerText(
```
**Step 3: Remove unused imports if any**
The `FastOutSlowInEasing` import (line 8) may now be unused. Check and remove if so. `RepeatMode` is still used (Restart).
**Step 4: Build the project**
Run: `./gradlew assembleDebug`
Expected: BUILD SUCCESSFUL
**Step 5: Commit**
```bash
git add app/src/main/java/xyz/cottongin/radio247/ui/screens/nowplaying/NowPlayingScreen.kt
git commit -m "feat: replace bounce marquee with constant-speed ticker
Replaces BounceMarqueeText with TickerText that scrolls at a fixed
33 dp/s regardless of text length. Text scrolls left off-screen with
a container-width gap before looping. Initial 1.5s delay lets the
user read the beginning."
```
### Task 2: Manual verification
**Step 1: Install and launch on device/emulator**
Run: `./gradlew installDebug`
**Step 2: Verify ticker behavior**
1. Play a station with a long track title
2. Confirm text scrolls smoothly leftward at a comfortable reading pace
3. Confirm text disappears off the left, gap passes, text reappears from start
4. Confirm short titles remain centered and static
5. Confirm the 1.5s initial pause is visible before scrolling starts