refactor: improve player gesture feedback with timed visibility components

- Replace `LaunchedEffect` and manual state handling for showing/hiding player overlays (brightness, volume, seek feedback) with new reusable timed visibility composables.
- Introduce `ValueChangeTimedVisibility` to automatically show brightness and volume sliders for a short duration when their values change.
- Implement `EmptyValueTimedVisibility` to display the horizontal seek amount indicator and hide it after a delay.
- Remove redundant state variables (`brightnessOverlayVisible`, `volumeOverlayVisible`, `showFeedbackPreview`) and related logic from `PlayerScreen`.
- Clean up `PlayerGesturesLayer` by removing the `setFeedBackPreview` callback, simplifying its responsibility.
- Remove the auto-hide `LaunchedEffect` from `PlayerSideSliders` as this is now handled externally.
This commit is contained in:
2026-01-27 19:16:23 +01:00
parent b97867d9f1
commit d24eff19cc
3 changed files with 28 additions and 47 deletions

View File

@@ -33,6 +33,8 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import hu.bbara.purefin.common.ui.components.EmptyValueTimedVisibility
import hu.bbara.purefin.common.ui.components.ValueChangeTimedVisibility
import hu.bbara.purefin.player.ui.components.PlayerControlsOverlay import hu.bbara.purefin.player.ui.components.PlayerControlsOverlay
import hu.bbara.purefin.player.ui.components.PlayerGesturesLayer import hu.bbara.purefin.player.ui.components.PlayerGesturesLayer
import hu.bbara.purefin.player.ui.components.PlayerLoadingErrorEndCard import hu.bbara.purefin.player.ui.components.PlayerLoadingErrorEndCard
@@ -40,7 +42,6 @@ import hu.bbara.purefin.player.ui.components.PlayerQueuePanel
import hu.bbara.purefin.player.ui.components.PlayerSettingsSheet import hu.bbara.purefin.player.ui.components.PlayerSettingsSheet
import hu.bbara.purefin.player.ui.components.PlayerSideSliders import hu.bbara.purefin.player.ui.components.PlayerSideSliders
import hu.bbara.purefin.player.viewmodel.PlayerViewModel import hu.bbara.purefin.player.viewmodel.PlayerViewModel
import kotlinx.coroutines.delay
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -61,18 +62,8 @@ fun PlayerScreen(
var volume by remember { mutableStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) / maxVolume.toFloat()) } var volume by remember { mutableStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) / maxVolume.toFloat()) }
var brightness by remember { mutableStateOf(readCurrentBrightness(activity)) } var brightness by remember { mutableStateOf(readCurrentBrightness(activity)) }
var showSettings by remember { mutableStateOf(false) } var showSettings by remember { mutableStateOf(false) }
var brightnessOverlayVisible by remember { mutableStateOf(false) }
var volumeOverlayVisible by remember { mutableStateOf(false) }
var showQueuePanel by remember { mutableStateOf(false) } var showQueuePanel by remember { mutableStateOf(false) }
var horizontalSeekFeedback by remember { mutableStateOf<Long?>(null) } var horizontalSeekFeedback by remember { mutableStateOf<Long?>(null) }
var showFeedbackPreview by remember { mutableStateOf(false) }
LaunchedEffect(showFeedbackPreview) {
if (!showFeedbackPreview) {
delay(1000)
horizontalSeekFeedback = null
}
}
LaunchedEffect(uiState.isPlaying) { LaunchedEffect(uiState.isPlaying) {
if (uiState.isPlaying) { if (uiState.isPlaying) {
@@ -111,22 +102,17 @@ fun PlayerScreen(
onVerticalDragLeft = { delta -> onVerticalDragLeft = { delta ->
val diff = (-delta / 800f) val diff = (-delta / 800f)
brightness = (brightness + diff).coerceIn(0f, 1f) brightness = (brightness + diff).coerceIn(0f, 1f)
brightnessOverlayVisible = true
applyBrightness(activity, brightness) applyBrightness(activity, brightness)
}, },
onVerticalDragRight = { delta -> onVerticalDragRight = { delta ->
val diff = (-delta / 800f) val diff = (-delta / 800f)
volume = (volume + diff).coerceIn(0f, 1f) volume = (volume + diff).coerceIn(0f, 1f)
volumeOverlayVisible = true
audioManager.setStreamVolume( audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC, AudioManager.STREAM_MUSIC,
(volume * maxVolume).roundToInt(), (volume * maxVolume).roundToInt(),
0 0
) )
}, },
setFeedBackPreview = {
showFeedbackPreview = it
},
onHorizontalDragPreview = { onHorizontalDragPreview = {
horizontalSeekFeedback = it horizontalSeekFeedback = it
}, },
@@ -136,30 +122,40 @@ fun PlayerScreen(
} }
) )
horizontalSeekFeedback?.let { delta -> EmptyValueTimedVisibility(
value = horizontalSeekFeedback,
hideAfterMillis = 1_000
) {
SeekAmountIndicator( SeekAmountIndicator(
deltaMs = delta, deltaMs = it,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
) )
} }
AnimatedVisibility( ValueChangeTimedVisibility(
visible = volumeOverlayVisible || brightnessOverlayVisible, value = brightness,
enter = fadeIn(), hideAfterMillis = 800
exit = fadeOut() ) { currentBrightness ->
) {
PlayerSideSliders( PlayerSideSliders(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize(), brightness = currentBrightness,
brightness = brightness,
volume = volume, volume = volume,
showBrightness = brightnessOverlayVisible, showBrightness = true,
showVolume = volumeOverlayVisible, showVolume = false,
onHide = { )
brightnessOverlayVisible = false }
volumeOverlayVisible = false
} ValueChangeTimedVisibility(
value = volume,
hideAfterMillis = 800
) { currentVolume ->
PlayerSideSliders(
modifier = Modifier.fillMaxSize(),
brightness = brightness,
volume = currentVolume,
showBrightness = false,
showVolume = true,
) )
} }

View File

@@ -21,7 +21,6 @@ fun PlayerGesturesLayer(
onVerticalDragRight: (delta: Float) -> Unit, onVerticalDragRight: (delta: Float) -> Unit,
onHorizontalDragPreview: (deltaMs: Long?) -> Unit = {}, onHorizontalDragPreview: (deltaMs: Long?) -> Unit = {},
onHorizontalDrag: (deltaMs: Long) -> Unit, onHorizontalDrag: (deltaMs: Long) -> Unit,
setFeedBackPreview: (show: Boolean) -> Unit
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val horizontalThresholdPx = with(density) { HorizontalSeekGestureHelper.START_THRESHOLD.toPx() } val horizontalThresholdPx = with(density) { HorizontalSeekGestureHelper.START_THRESHOLD.toPx() }
@@ -66,14 +65,12 @@ fun PlayerGesturesLayer(
accumulatedHorizontalDrag = 0f accumulatedHorizontalDrag = 0f
isHorizontalDragActive = false isHorizontalDragActive = false
lastPreviewDelta = null lastPreviewDelta = null
setFeedBackPreview(false)
onHorizontalDragPreview(null) onHorizontalDragPreview(null)
}, },
onHorizontalDrag = { change, dragAmount -> onHorizontalDrag = { change, dragAmount ->
accumulatedHorizontalDrag += dragAmount accumulatedHorizontalDrag += dragAmount
if (!isHorizontalDragActive && kotlin.math.abs(accumulatedHorizontalDrag) >= horizontalThresholdPx) { if (!isHorizontalDragActive && kotlin.math.abs(accumulatedHorizontalDrag) >= horizontalThresholdPx) {
isHorizontalDragActive = true isHorizontalDragActive = true
setFeedBackPreview(true)
} }
if (isHorizontalDragActive) { if (isHorizontalDragActive) {
change.consume() change.consume()
@@ -95,14 +92,12 @@ fun PlayerGesturesLayer(
accumulatedHorizontalDrag = 0f accumulatedHorizontalDrag = 0f
isHorizontalDragActive = false isHorizontalDragActive = false
lastPreviewDelta = null lastPreviewDelta = null
setFeedBackPreview(false)
onHorizontalDragPreview(null) onHorizontalDragPreview(null)
}, },
onDragCancel = { onDragCancel = {
accumulatedHorizontalDrag = 0f accumulatedHorizontalDrag = 0f
isHorizontalDragActive = false isHorizontalDragActive = false
lastPreviewDelta = null lastPreviewDelta = null
setFeedBackPreview(false)
onHorizontalDragPreview(null) onHorizontalDragPreview(null)
} }
) )

View File

@@ -15,12 +15,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable @Composable
fun PlayerSideSliders( fun PlayerSideSliders(
@@ -29,17 +27,9 @@ fun PlayerSideSliders(
volume: Float, volume: Float,
showBrightness: Boolean, showBrightness: Boolean,
showVolume: Boolean, showVolume: Boolean,
onHide: () -> Unit
) { ) {
val scheme = MaterialTheme.colorScheme val scheme = MaterialTheme.colorScheme
LaunchedEffect(showBrightness, showVolume) {
if (showBrightness || showVolume) {
delay(800)
onHide()
}
}
Box(modifier = modifier.fillMaxWidth()) { Box(modifier = modifier.fillMaxWidth()) {
if (showBrightness) { if (showBrightness) {
SideOverlay( SideOverlay(