From 869bb27497ee34bbf9cebec61c06455d9e3bd518 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Tue, 21 Jul 2026 17:01:35 +0000 Subject: [PATCH] feat(player): add sticky auto-brightness zone with hysteresis Refactor brightness drag to use a normal 0-1 sliding range with a sticky auto-brightness zone at the bottom. Enter/exit auto mode via hysteresis thresholds (15% overshoot past 0%) so accidental drags don't toggle the mode. Lock the current system brightness on screen open (reading the actual value even when in auto mode) and restore the original window brightness on ON_STOP. Add vertical drag lifecycle callbacks (start/end) to PlayerGesturesLayer so overshoot can be reset per gesture. Extract drag handlers into local functions for readability. --- .../purefin/ui/screen/player/PlayerScreen.kt | 124 ++++++++++++++---- .../player/components/PlayerGesturesLayer.kt | 11 ++ 2 files changed, 106 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt index e7cc82e2..791190b8 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/PlayerScreen.kt @@ -3,6 +3,7 @@ package hu.bbara.purefin.ui.screen.player import android.app.Activity import android.content.Context import android.media.AudioManager +import android.provider.Settings import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -67,8 +68,9 @@ import kotlin.math.abs import kotlin.math.roundToInt private const val CONTROLS_VISIBLE_SUBTITLE_BOTTOM_PADDING_FRACTION = 0.32f -/** Small negative band below zero that represents adaptive/auto brightness mode. */ -private const val AUTO_BRIGHTNESS_THRESHOLD = -0.15f +private const val BRIGHTNESS_DRAG_SENSITIVITY = 800f +private const val BRIGHTNESS_AUTO_ENTER_THRESHOLD = 0.15f +private const val BRIGHTNESS_AUTO_EXIT_THRESHOLD = 0.15f private const val CONTROLS_AUTO_HIDE_MS = 3_000L @OptIn(UnstableApi::class) @@ -90,7 +92,11 @@ fun PlayerScreen( val audioManager = remember { context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } val maxVolume = remember { audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC).coerceAtLeast(1) } var volume by remember { mutableFloatStateOf(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) / maxVolume.toFloat()) } - var brightness by remember { mutableFloatStateOf(readCurrentBrightness(activity)) } + var brightness by remember { mutableFloatStateOf(readCurrentBrightness(activity, context)) } + var isAutoBrightness by remember { mutableStateOf(false) } + var dragOvershoot by remember { mutableFloatStateOf(0f) } + var autoDragTick by remember { mutableStateOf(0) } + val originalScreenBrightness by remember { mutableFloatStateOf(activity?.window?.attributes?.screenBrightness ?: -1f) } var showQueuePanel by remember { mutableStateOf(false) } var horizontalSeekFeedback by remember { mutableStateOf(null) } var horizontalSeekPreviewPositionMs by remember { mutableStateOf(null) } @@ -131,9 +137,61 @@ fun PlayerScreen( toggleControlsVisibility() hideControlsWithTimeout() } + fun onBrightnessDragStart(isLeftSide: Boolean) { + if (isLeftSide) { + dragOvershoot = 0f + } + } + fun onBrightnessDragEnd() { + dragOvershoot = 0f + } + fun onBrightnessDrag(delta: Float) { + val diff = -delta / BRIGHTNESS_DRAG_SENSITIVITY + if (isAutoBrightness) { + if (diff > 0f) { + dragOvershoot += diff + if (dragOvershoot >= BRIGHTNESS_AUTO_EXIT_THRESHOLD) { + isAutoBrightness = false + brightness = (dragOvershoot - BRIGHTNESS_AUTO_EXIT_THRESHOLD).coerceIn(0f, 1f) + dragOvershoot = 0f + applyBrightness(activity, brightness, isAuto = false) + } + } else { + autoDragTick++ + } + } else { + val newBrightness = brightness + diff + if (newBrightness <= 0f) { + brightness = 0f + applyBrightness(activity, brightness, isAuto = false) + dragOvershoot += newBrightness + if (dragOvershoot <= -BRIGHTNESS_AUTO_ENTER_THRESHOLD) { + isAutoBrightness = true + dragOvershoot = 0f + applyBrightness(activity, brightness, isAuto = true) + } + } else { + brightness = newBrightness.coerceIn(0f, 1f) + dragOvershoot = 0f + applyBrightness(activity, brightness, isAuto = false) + } + } + } + fun onVolumeDrag(delta: Float) { + val diff = -delta / 800f + volume = (volume + diff).coerceIn(0f, 1f) + audioManager.setStreamVolume( + AudioManager.STREAM_MUSIC, + (volume * maxVolume).roundToInt(), + 0 + ) + } LifecycleEventEffect(Lifecycle.Event.ON_START) { hideControlsWithTimeout() } + LifecycleEventEffect(Lifecycle.Event.ON_STOP) { + restoreBrightness(activity, originalScreenBrightness) + } Box( @@ -169,20 +227,10 @@ fun PlayerScreen( onDoubleTapRight = { viewModel.seekBy(30_000) }, onDoubleTapLeft = { viewModel.seekBy(-10_000) }, onDoubleTapCenter = { viewModel.togglePlayPause() }, - onVerticalDragLeft = { delta -> - val diff = (-delta / 800f) - brightness = (brightness + diff).coerceIn(AUTO_BRIGHTNESS_THRESHOLD, 1f) - applyBrightness(activity, brightness) - }, - onVerticalDragRight = { delta -> - val diff = (-delta / 800f) - volume = (volume + diff).coerceIn(0f, 1f) - audioManager.setStreamVolume( - AudioManager.STREAM_MUSIC, - (volume * maxVolume).roundToInt(), - 0 - ) - }, + onVerticalDragStart = ::onBrightnessDragStart, + onVerticalDragEnd = ::onBrightnessDragEnd, + onVerticalDragLeft = ::onBrightnessDrag, + onVerticalDragRight = ::onVolumeDrag, onHorizontalDragPreview = { deltaMs, previewPositionMs -> horizontalSeekFeedback = deltaMs horizontalSeekPreviewPositionMs = previewPositionMs?.let { @@ -230,23 +278,21 @@ fun PlayerScreen( } ValueChangeTimedVisibility( - value = brightness, + value = BrightnessUiState(brightness, isAutoBrightness, autoDragTick), hideAfterMillis = 800, modifier = Modifier .align(Alignment.CenterStart) .padding(start = 20.dp) - ) { currentBrightness -> + ) { state -> PlayerAdjustmentIndicator( modifier = Modifier.align(Alignment.Center), icon = Icons.Outlined.BrightnessMedium, contentDescription = "Brightness", - value = currentBrightness, - bottomText = if (currentBrightness <= AUTO_BRIGHTNESS_THRESHOLD) { + value = state.brightness, + bottomText = if (state.isAuto) { "Auto" - } else if (currentBrightness >= 0f) { - "${(currentBrightness * 100).roundToInt()}%" } else { - null + "${(state.brightness * 100).roundToInt()}%" } ) } @@ -434,14 +480,34 @@ private fun formatSeekDelta(deltaMs: Long): String { } } -private fun readCurrentBrightness(activity: Activity?): Float { - val current = activity?.window?.attributes?.screenBrightness - return if (current != null && current >= 0) current else -1f +private data class BrightnessUiState(val brightness: Float, val isAuto: Boolean, val dragTick: Int) + +private fun readCurrentBrightness(activity: Activity?, context: Context): Float { + val windowBrightness = activity?.window?.attributes?.screenBrightness + if (windowBrightness != null && windowBrightness >= 0f) { + return windowBrightness + } + return try { + val systemBrightness = Settings.System.getInt( + context.contentResolver, + Settings.System.SCREEN_BRIGHTNESS + ) + (systemBrightness / 255f).coerceIn(0f, 1f) + } catch (e: Settings.SettingNotFoundException) { + 0.5f + } } -private fun applyBrightness(activity: Activity?, value: Float) { +private fun applyBrightness(activity: Activity?, brightness: Float, isAuto: Boolean) { activity ?: return val params = activity.window.attributes - params.screenBrightness = if (value <= AUTO_BRIGHTNESS_THRESHOLD) -1f else value + params.screenBrightness = if (isAuto) -1f else brightness + activity.window.attributes = params +} + +private fun restoreBrightness(activity: Activity?, originalValue: Float) { + activity ?: return + val params = activity.window.attributes + params.screenBrightness = originalValue activity.window.attributes = params } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerGesturesLayer.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerGesturesLayer.kt index 4716967b..51a963bf 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerGesturesLayer.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/player/components/PlayerGesturesLayer.kt @@ -30,6 +30,8 @@ fun PlayerGesturesLayer( onDoubleTapLeft: () -> Unit, onVerticalDragLeft: (delta: Float) -> Unit, onVerticalDragRight: (delta: Float) -> Unit, + onVerticalDragStart: (isLeftSide: Boolean) -> Unit = {}, + onVerticalDragEnd: () -> Unit = {}, onHorizontalDragPreview: (deltaMs: Long?, previewPositionMs: Long?) -> Unit = { _, _ -> }, onHorizontalDragSeekTo: (positionMs: Long) -> Unit, currentPositionProvider: () -> Long, @@ -79,6 +81,7 @@ fun PlayerGesturesLayer( var isHorizontalDragActive = false var lastPreviewDelta: Long? = null var startPositionMs = 0L + var verticalDragStarted = false drag(down.id) { change -> val delta = change.positionChange() @@ -115,6 +118,10 @@ fun PlayerGesturesLayer( DragDirection.VERTICAL -> { change.consume() val isLeftSide = startX < size.width / 2 + if (!verticalDragStarted) { + verticalDragStarted = true + onVerticalDragStart(isLeftSide) + } if (isLeftSide) { onVerticalDragLeft(delta.y) } else { @@ -134,6 +141,10 @@ fun PlayerGesturesLayer( } } onHorizontalDragPreview(null, null) + + if (verticalDragStarted) { + onVerticalDragEnd() + } } } )