Add ticker text implementation plan
Two-task plan: replace BounceMarqueeText with TickerText composable, then manually verify on device. Made-with: Cursor
This commit is contained in:
168
docs/plans/2026-03-11-ticker-text-implementation.md
Normal file
168
docs/plans/2026-03-11-ticker-text-implementation.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# 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 449–543) 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
|
||||||
Reference in New Issue
Block a user