# 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