Two-task plan: replace BounceMarqueeText with TickerText composable, then manually verify on device. Made-with: Cursor
5.9 KiB
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:
@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:
BounceMarqueeText(
To:
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
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
- Play a station with a long track title
- Confirm text scrolls smoothly leftward at a comfortable reading pace
- Confirm text disappears off the left, gap passes, text reappears from start
- Confirm short titles remain centered and static
- Confirm the 1.5s initial pause is visible before scrolling starts