Roku-style fixed-focus D-pad navigation, in pure Jetpack Compose. The highlight stays locked. The content glides beneath it.
Android TV moves focus to every item. Roku never does. The highlight stays locked. The content glides. RokuFocus brings that to Compose.
Compose TV's default moves focus to each item, so the whole row jumps around. RokuFocus flips it — the same pattern Roku and every major streaming app ships.
Focus chases items across the screen, then the list lurches to catch up. Your eye has to track two moving things.
The highlight never moves. Content glides under a fixed anchor — predictable, cinematic, easy to track from ten feet away.
A live RokuFocus row, wired to your keyboard. Every control below maps to a real RokuFocusConfig parameter.
Click the demo, then press ←→ to navigate · Enter to select. Hit an edge with wrap off to feel the boundary.
No Views, no fragments, no fighting the focus engine. Drop it in and ship.
Content scrolls under a stationary ring — selection is tracked in state, not the Compose focus system.
RokuLazyColumn morphs one highlight across rows of different card sizes — X, Y, width, and height all animate.
Hold the D-pad: presses are throttled, then accelerate after three in a row. Long lists stop feeling long.
Last to first with one flag. Boundary hits vibrate, so the edge is something users feel, not guess.
The ring is a BoxScope lambda. Keep the default, restyle it, or replace it with anything you can compose.
Only Foundation, UI, Animation, and Runtime. Works with any design system, back to minSdk 24.
Rows, grids, and config — straight from the library.
// A fixed-focus row. Item width is auto-measured. RokuLazyRow( itemSpacing = 14.dp, onItemClicked = { index -> play(index) } ) { items(movies) { movie, isFocused -> MovieCard(movie, isFocused) } }
// A full OTT home screen: mixed card sizes, one morphing highlight. RokuLazyColumn { row(itemWidth = 580.dp, itemHeight = 310.dp, header = { Text("Hero") }) { items(heroMovies) { m, focused -> BannerCard(m, focused) } } row(itemWidth = 220.dp, itemHeight = 140.dp, header = { Text("Trending Now") }) { items(trending) { m, focused -> MovieCard(m, focused) } } }
// Tune the feel — globally or per component. val config = RokuFocusConfig( highlightAnimationSpec = RokuAnimationSpec.Smooth, keyRepeatDelayMs = 150L, keyRepeatAccelAfter = 3, // speed up after 3 presses keyRepeatFastDelayMs = 50L, wrapAround = true, hapticFeedback = true )
Add JitPack, add the dependency, press right.
// settings.gradle.kts dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } } } // build.gradle.kts dependencies { implementation("com.github.souravnoobcoder:roku-focus-list:1.0.0") }
Open source. Apache 2.0. Built for the ten-foot experience.