Documentation Index
Fetch the complete documentation index at: https://mintlify.com/6xingyv/accompanist-lyrics-ui/llms.txt
Use this file to discover all available pages before exploring further.
PlayerScreen.kt is the heart of the sample app and demonstrates several patterns that are strongly recommended for production use of KaraokeLyricsView. Reading through it end-to-end will show you how to wire up a frame-accurate position provider, configure the glow blend mode correctly, handle adaptive layouts, integrate the flowing-light background, and hook the share sheet into the lyric long-press callback — all without triggering unnecessary recompositions.
The Frame-Loop Position Provider
This is the single most important pattern in the entire sample. Getting it right is what separates a silky-smooth karaoke animation from a janky, stuttering one.Why a custom position provider?
KaraokeLyricsView accepts a currentPosition: () -> Int lambda rather than a plain Int parameter. This is deliberate: if the position were a normal @Composable parameter, every millisecond change in playback time would trigger a full recomposition of PlayerScreen — cascading through every child composable and thrashing the composition system.
Instead, the lambda is read only inside the Canvas within KaraokeLyricsView. Updating a MutableLongState value causes only the Canvas to redraw, leaving PlayerScreen and all its sibling composables completely untouched.
How it works
Step-by-step breakdown
Create a MutableLongState
val animatedPositionState = remember { mutableLongStateOf(0L) } allocates a single Long-backed Compose state object. It survives recompositions because it is wrapped in remember.Wrap it in a stable lambda
val currentPositionProvider = remember { { animatedPositionState.longValue.toInt() } } creates a lambda that is allocated once. Because it is wrapped in remember it will never change identity across recompositions, keeping KaraokeLyricsView’s parameter stable.Run the frame loop
LaunchedEffect(isPlaying) launches a coroutine that lives exactly as long as isPlaying remains true. Inside the loop, awaitFrame() suspends the coroutine until the next VSYNC signal from Android’s Choreographer, ensuring the position is updated at display refresh rate (typically 60 or 120 Hz).Interpolate position between ViewModel snapshots
PlayerViewModel snapshots MediaController.currentPosition every 250 ms. The frame loop bridges those discrete snapshots by computing position + (currentTime - lastUpdateTime), producing a smooth monotonically-increasing time value on every frame. The abs(newPosition - currentAnimPos) >= 100 guard allows backward jumps (seek operations) to pass through immediately.Glow Effect Setup
The sample renders lyrics with a luminous glow on top of theFlowingLightBackground. Achieving this correctly requires two specific settings working together.
Key settings explained
| Setting | Value | Why |
|---|---|---|
blendMode | BlendMode.Plus | Additive blending: white text on a dark background glows; overlapping layers brighten naturally — the same technique Apple Music uses |
compositingStrategy | CompositingStrategy.Offscreen | Forces the composable and all its children to be rendered into an offscreen buffer first, then composited onto the parent with the specified blend mode. Without this, BlendMode.Plus produces incorrect results because it would blend against whatever happens to be in the parent layer |
useBlurEffect | false | The sample disables KaraokeLyricsView’s built-in blur halo because the BlendMode.Plus glow is already visually dramatic on top of the blurred FlowingLightBackground. Enable it if you want a softer double-glow look |
Adaptive Layout
PlayerScreen reads LocalWindowLayoutType.current to decide which layout to render. There is no manual breakpoint arithmetic — the LocalWindowLayoutType composition local is provided by the app’s adaptive layout helper and automatically reflects the current window class.
Phone layout — MobilePlayerScreen
On a phone in portrait orientation (WindowLayoutType.Phone), the screen shows a compact header row containing the album-art thumbnail, scrolling title/artist marquee text, and the controls cluster. Below the header, PlayerLyrics fills the remaining vertical space with the full-screen karaoke view.
MobilePlayerScreen composable renders:
- A
Rowheader with a 60 dp album-art thumbnail clipped to aContinuousRoundedRectangle(6.dp),PlayerMetadata(title + artist inBlendMode.Plus), andPlayerControls(translation toggle, phonetic toggle, song-selection button) PlayerLyricswithModifier.padding(horizontal = 6.dp)below it
Tablet / Desktop layout — PadPlayerScreen
On larger windows (WindowLayoutType.Pad / desktop), a Row places a 40 % wide Column on the left containing the full album-art image (aspect ratio 1:1) and the metadata/controls below it. The lyrics occupy the remaining horizontal space on the right, wrapped in AnimatedVisibility so they fade in once lyrics have loaded.
Both layouts receive the same
currentPositionProvider lambda. Because the lambda identity is stable (created with remember once in PlayerScreen), switching between layouts does not create a new lambda or reset the frame loop.FlowingLightBackground
TheFlowingLightBackground composable creates a cinematic depth effect by rendering multiple blurred, scaled copies of the album-art bitmap layered on top of each other. It receives a BackgroundVisualState which carries:
bitmap: ImageBitmap?— the album art decoded fromMediaController.mediaMetadata.artworkDataluminance: Float— the average perceived brightness of the bitmap (0–1), calculated inPlayerViewModelon a background coroutine using the ITU-R BT.709 formula (0.2126·R + 0.7152·G + 0.0722·B)
luminance > 0.5 (a bright album art), FlowingLightBackground applies a ColorMatrix that desaturates and darkens the image so the white lyric text remains readable. Bitmap transitions use a 600 ms cross-fade via AnimatedContent.
In PlayerScreen, the background state is derived cheaply without triggering PlayerScreen recomposition:
Share Sheet
Long-pressing a lyric line triggers the share flow.PlayerLyrics exposes this through the onLinePressed callback of KaraokeLyricsView:
onShare lambda in MobilePlayerScreen (and PadPlayerScreen) does three things in sequence:
ShareScreen is shown inside a ModalScaffold driven by uiState.isShareSheetVisible. It has two steps:
ShareStep.SELECTING— a scrollable list of all lyric lines; the user taps to toggle up to five lines for inclusion in the share cardShareStep.GENERATING— a horizontal pager with two card styles (Apple-inspired and Spotify-inspired). Users can toggle translation/phonetic display, save the card to the gallery, or invoke the system share sheet
PlayerViewModel.onShareDismissed() resumes playback and ShareViewModel.reset() clears the state.
Song Selection
TheMusicItemSelectionDialog composable provides a two-level selection UI:
- The bundled track list (
LazyColumn) shows eachMusicItem’slabelandtestTargetdescription. Tapping a track callsPlayerViewModel.onSongSelected(item), which callsMediaController.setMediaItem, setsrepeatMode = Player.REPEAT_MODE_ALL, then callsprepare()andplay(), and finally triggersloadLyricsFor(item)to parse the lyrics asset asynchronously. - The “Select Custom File…” button opens
CustomSongSelectionDialog, which usesActivityResultContracts.GetContentto let the user pick an audio file, a lyrics file, and an optional translation file from device storage. The content resolver reads the file bytes inline and constructs aMusicItemwithisCustom = true.
MusicRepositoryImpl.getLyricsFor checks item.isCustom to decide whether to read from assets or use the embedded string directly, so the same parsing pipeline handles both cases transparently.