@@ -54,6 +54,7 @@ import androidx.compose.runtime.key
5454import androidx.compose.runtime.mutableStateOf
5555import androidx.compose.runtime.remember
5656import androidx.compose.runtime.rememberCoroutineScope
57+ import androidx.compose.runtime.rememberUpdatedState
5758import androidx.compose.runtime.setValue
5859import androidx.compose.ui.Alignment
5960import androidx.compose.ui.Modifier
@@ -104,6 +105,7 @@ import kotlinx.coroutines.CoroutineScope
104105import kotlinx.coroutines.Dispatchers
105106import kotlinx.coroutines.SupervisorJob
106107import kotlinx.coroutines.cancel
108+ import kotlinx.coroutines.delay
107109import kotlinx.coroutines.flow.MutableStateFlow
108110import kotlinx.coroutines.flow.StateFlow
109111import kotlinx.coroutines.launch
@@ -472,7 +474,8 @@ private fun MiniMediaPlayer(
472474 }
473475
474476 SeekBarCompose (
475- progressFraction = progressFraction(state),
477+ progressMs = progressMs,
478+ durationMs = durationMs,
476479 isPlaying = state.isMediaPlaying,
477480 onSeek = onSeek,
478481 modifier = Modifier .fillMaxWidth().height(28 .dp)
@@ -571,13 +574,38 @@ private fun ControlButton(
571574
572575@Composable
573576private fun SeekBarCompose (
574- progressFraction : Float ,
577+ progressMs : Long ,
578+ durationMs : Long ,
575579 isPlaying : Boolean ,
576580 onSeek : (Float ) -> Unit ,
577581 modifier : Modifier = Modifier ,
578582) {
579583 var isScrubbing by remember { mutableStateOf(false ) }
580584
585+ val onSeekRef = rememberUpdatedState(onSeek)
586+
587+ val serverFraction = if (durationMs > 0 ) (progressMs.toFloat() / durationMs).coerceIn(0f , 1f ) else 0f
588+ var displayFraction by remember { mutableStateOf(serverFraction) }
589+
590+ LaunchedEffect (progressMs, durationMs, isPlaying) {
591+ if (isScrubbing) return @LaunchedEffect
592+
593+ displayFraction = serverFraction
594+
595+ if (! isPlaying || durationMs <= 0 ) return @LaunchedEffect
596+
597+ val startWallMs = System .currentTimeMillis()
598+ val startProgressMs = progressMs
599+ while (true ) {
600+ delay(16L ) // ~60 fps
601+ if (isScrubbing) break
602+ val elapsed = System .currentTimeMillis() - startWallMs
603+ val interpolated = ((startProgressMs + elapsed).toFloat() / durationMs).coerceIn(0f , 1f )
604+ displayFraction = interpolated
605+ if (interpolated >= 1f ) break
606+ }
607+ }
608+
581609 AndroidView (
582610 factory = { ctx ->
583611 SeekBar (ctx).apply {
@@ -632,26 +660,25 @@ private fun SeekBarCompose(
632660
633661 setOnSeekBarChangeListener(
634662 object : SeekBar .OnSeekBarChangeListener {
635- override fun onProgressChanged (sb : SeekBar ? , v : Int , fromUser : Boolean ) {
636- if (fromUser) onSeek(v / 10_000f )
637- }
663+ override fun onProgressChanged (sb : SeekBar ? , v : Int , fromUser : Boolean ) = Unit
638664
639665 override fun onStartTrackingTouch (sb : SeekBar ? ) {
640666 isScrubbing = true
641667 }
642668
643669 override fun onStopTrackingTouch (sb : SeekBar ? ) {
670+ sb?.let { onSeekRef.value(it.progress / 10_000f ) }
644671 isScrubbing = false
645672 }
646673 }
647674 )
648675 }
649676 },
650677 update = { bar ->
651- val target = (progressFraction * 10_000f ).toInt().coerceIn(0 , 10_000 )
678+ val target = (displayFraction * 10_000f ).toInt().coerceIn(0 , 10_000 )
652679
653680 if (! isScrubbing) {
654- // Keep QS reset behavior if you want it
681+ // Keep QS reset behaviour for track-restart detection.
655682 if (
656683 target <= SeekBarObserver .RESET_ANIMATION_THRESHOLD_MS &&
657684 bar.progress > SeekBarObserver .RESET_ANIMATION_THRESHOLD_MS
0 commit comments