Skip to content

Commit

Permalink
refactor(gestures): better volume and brightness gestures
Browse files Browse the repository at this point in the history
  • Loading branch information
abdallahmehiz committed Jun 24, 2024
1 parent 9121fa4 commit 19d5ae6
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 23 deletions.
10 changes: 10 additions & 0 deletions app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.util.Log
import android.view.KeyEvent
import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
Expand Down Expand Up @@ -323,4 +324,13 @@ class PlayerActivity : AppCompatActivity() {
PlayerOrientation.SensorLandscape -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
}

override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { viewModel.changeVolumeBy(1) }
KeyEvent.KEYCODE_VOLUME_DOWN -> { viewModel.changeVolumeBy(-1) }
else -> { super.onKeyDown(keyCode, event) }
}
return true
}
}
33 changes: 26 additions & 7 deletions app/src/main/java/live/mehiz/mpvkt/ui/player/PlayerViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class PlayerViewModel(
val areControlsLocked = _areControlsLocked.asStateFlow()

val playerUpdate = MutableStateFlow(PlayerUpdates.None)
val isBrightnessSliderShown = MutableStateFlow(false)
val isVolumeSliderShown = MutableStateFlow(false)
val currentBrightness = MutableStateFlow(activity.window.attributes.screenBrightness)
val currentVolume = MutableStateFlow(activity.audioManager.getStreamVolume(AudioManager.STREAM_MUSIC))

val sheetShown = MutableStateFlow(Sheets.None)
val gestureSeekAmount = MutableStateFlow(0)
Expand Down Expand Up @@ -217,7 +221,7 @@ class PlayerViewModel(

fun pause() {
activity.player.paused = true
_paused.value = true
_paused.update { true }
}

fun unpause() {
Expand Down Expand Up @@ -272,20 +276,35 @@ class PlayerViewModel(
isLoading.update { true }
}

fun changeBrightnessWithDrag(
dragAmount: Float,
fun changeBrightnessBy(change: Float) {
changeBrightnessTo(currentBrightness.value + change)
}

fun changeBrightnessTo(
brightness: Float,
) {
isBrightnessSliderShown.update { true }
currentBrightness.update { brightness.coerceIn(0f, 1f) }
activity.window.attributes = activity.window.attributes.apply {
screenBrightness = dragAmount.coerceIn(0f, 1f)
screenBrightness = brightness.coerceIn(0f, 1f)
}
}

fun changeVolumeWithDrag(dragAmount: Float) {
fun changeVolumeBy(change: Int) {
changeVolumeTo(currentVolume.value + change)
}

val maxVolume = activity.audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
fun changeVolumeTo(volume: Int) {
isVolumeSliderShown.update { true }
val newVolume = volume.coerceIn(0..maxVolume)
activity.audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
dragAmount.toInt(),
AudioManager.FLAG_SHOW_UI,
newVolume,
0,
)
println(activity.audioManager.getStreamVolume(AudioManager.STREAM_MUSIC))
currentVolume.update { newVolume }
}

fun changeVideoAspect(aspect: VideoAspect) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ fun GestureHandler(
viewModel.showSeekBar()
}
var isLongPressing by remember { mutableStateOf(false) }
val currentVolume by viewModel.currentVolume.collectAsState()
val currentBrightness by viewModel.currentBrightness.collectAsState()
Box(
modifier = Modifier
.fillMaxSize()
Expand Down Expand Up @@ -163,23 +165,32 @@ fun GestureHandler(
}
.pointerInput(Unit) {
if ((!brightnessGesture && !volumeGesture) || areControlsLocked) return@pointerInput
var dragAmount = 0f
var startingY = 0f
var originalVolume = currentVolume
var originalBrightness = currentBrightness
detectVerticalDragGestures(
onDragEnd = { dragAmount = 0f },
onDragEnd = { startingY = 0f },
onDragStart = {
startingY = it.y
originalVolume = currentVolume
originalBrightness = currentBrightness
}
) { change, amount ->
dragAmount -= amount / 10
when {
volumeGesture && brightnessGesture -> {
if (change.position.x < size.width / 2) viewModel.changeBrightnessWithDrag(dragAmount)
else viewModel.changeVolumeWithDrag(dragAmount)
if (change.position.x < size.width / 2) {
viewModel.changeBrightnessTo(originalBrightness + ((startingY - change.position.y) * 0.005f))
} else {
viewModel.changeVolumeTo(originalVolume + ((startingY - change.position.y) * 0.1f).toInt())
}
}

brightnessGesture -> {
viewModel.changeBrightnessWithDrag(dragAmount)
viewModel.changeBrightnessTo(originalBrightness + ((startingY - change.position.y) * 0.005f))
}
// it's not always true, AS is drunk
volumeGesture -> {
viewModel.changeVolumeWithDrag(dragAmount)
viewModel.changeVolumeTo(originalVolume + ((startingY - change.position.y) * 0.1f).toInt())
}

else -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.LockOpen
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -53,10 +54,12 @@ import live.mehiz.mpvkt.preferences.PlayerPreferences
import live.mehiz.mpvkt.preferences.preference.collectAsState
import live.mehiz.mpvkt.ui.player.PlayerUpdates
import live.mehiz.mpvkt.ui.player.PlayerViewModel
import live.mehiz.mpvkt.ui.player.controls.components.BrightnessSlider
import live.mehiz.mpvkt.ui.player.controls.components.ControlsButton
import live.mehiz.mpvkt.ui.player.controls.components.DoubleSpeedPlayerUpdate
import live.mehiz.mpvkt.ui.player.controls.components.SeekbarWithTimers
import live.mehiz.mpvkt.ui.player.controls.components.TextPlayerUpdate
import live.mehiz.mpvkt.ui.player.controls.components.VolumeSlider
import live.mehiz.mpvkt.ui.theme.PlayerRippleTheme
import live.mehiz.mpvkt.ui.theme.spacing
import org.koin.compose.koinInject
Expand Down Expand Up @@ -103,6 +106,7 @@ class PlayerControls(private val viewModel: PlayerViewModel) {
CompositionLocalProvider(
LocalRippleTheme provides PlayerRippleTheme,
LocalPlayerButtonsClickEvent provides { resetControls = !resetControls },
LocalContentColor provides Color.White
) {
ConstraintLayout(
modifier = Modifier
Expand All @@ -120,8 +124,46 @@ class PlayerControls(private val viewModel: PlayerViewModel) {
bottomLeftControls,
bottomRightControls,
unlockControlsButton,
brightnessSlider,
volumeSlider
) = createRefs()

val isBrightnessSliderShown by viewModel.isBrightnessSliderShown.collectAsState()
val isVolumeSliderShown by viewModel.isVolumeSliderShown.collectAsState()
val brightness by viewModel.currentBrightness.collectAsState()
val volume by viewModel.currentVolume.collectAsState()
LaunchedEffect(volume, brightness) {
delay(1000)
if (isVolumeSliderShown) viewModel.isVolumeSliderShown.update { false }
if (isBrightnessSliderShown) viewModel.isBrightnessSliderShown.update { false }
}
AnimatedVisibility(
isBrightnessSliderShown,
enter = slideInHorizontally { it } + fadeIn(),
exit = slideOutHorizontally { it } + fadeOut(),
modifier = Modifier.constrainAs(brightnessSlider) {
end.linkTo(parent.end, spacing.medium)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}
) { BrightnessSlider(brightness, 0f..1f) }

AnimatedVisibility(
isVolumeSliderShown,
enter = slideInHorizontally { -it } + fadeIn(),
exit = slideOutHorizontally { -it } + fadeOut(),
modifier = Modifier.constrainAs(volumeSlider) {
start.linkTo(parent.start, spacing.medium)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
}
) {
VolumeSlider(
volume,
0..viewModel.maxVolume
)
}

val currentPlayerUpdate by viewModel.playerUpdate.collectAsState()
val aspectRatio by playerPreferences.videoAspect.collectAsState()
LaunchedEffect(currentPlayerUpdate, aspectRatio) {
Expand All @@ -139,8 +181,7 @@ class PlayerControls(private val viewModel: PlayerViewModel) {
linkTo(parent.top, parent.bottom, bias = 0.2f)
},
) {
val latestOne by remember { mutableStateOf(currentPlayerUpdate) }
when (latestOne) {
when (currentPlayerUpdate) {
PlayerUpdates.DoubleSpeed -> DoubleSpeedPlayerUpdate()
PlayerUpdates.AspectRatio -> TextPlayerUpdate(stringResource(aspectRatio.titleRes))
else -> {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,7 @@ fun PlayerUpdate(
.padding(vertical = 8.dp, horizontal = 16.dp)
.animateContentSize(),
contentAlignment = Alignment.Center,
) {
CompositionLocalProvider(
LocalContentColor provides Color.White,
) {
content()
}
}
) { content() }
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package live.mehiz.mpvkt.ui.player.controls.components

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.VolumeDown
import androidx.compose.material.icons.automirrored.filled.VolumeMute
import androidx.compose.material.icons.automirrored.filled.VolumeOff
import androidx.compose.material.icons.automirrored.filled.VolumeUp
import androidx.compose.material.icons.filled.BrightnessHigh
import androidx.compose.material.icons.filled.BrightnessLow
import androidx.compose.material.icons.filled.BrightnessMedium
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

val percentage: (Float, ClosedFloatingPointRange<Float>) -> Float = { value, range ->
((value - range.start) / (range.endInclusive - range.start)).coerceIn(0f, 1f)
}

val percentageInt: (Int, ClosedRange<Int>) -> Float = { value, range ->
((value - range.start - 0f) / (range.endInclusive - range.start)).coerceIn(0f, 1f)
}

@Composable
@Preview
fun VerticalSlider(
value: Float = 5f,
range: ClosedFloatingPointRange<Float> = 0f..100f
) {
require(range.contains(value)) { "Value must be within the provided range" }
Box(
modifier = Modifier
.height(120.dp)
.aspectRatio(0.2f)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.BottomCenter
) {
val targetHeight by animateFloatAsState(percentage(value, range), label = "")
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight(targetHeight)
.background(MaterialTheme.colorScheme.tertiary)
)
}
}


@Composable
@Preview
fun VerticalSlider(
value: Int = 5,
range: ClosedRange<Int> = 0..100
) {
require(range.contains(value)) { "Value must be within the provided range" }
Box(
modifier = Modifier
.height(120.dp)
.aspectRatio(0.2f)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.BottomCenter
) {
val targetHeight by animateFloatAsState(percentageInt(value, range), label = "")
Box(
Modifier
.fillMaxWidth()
.fillMaxHeight(targetHeight)
.background(MaterialTheme.colorScheme.tertiary)
)
}
}

@Composable
fun BrightnessSlider(
brightness: Float,
range: ClosedFloatingPointRange<Float>,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
(brightness * 100).toInt().toString(),
style = MaterialTheme.typography.bodySmall
)
VerticalSlider(
brightness,
range
)
Icon(
when (percentage(brightness, range)) {
in 0f..0.3f -> Icons.Default.BrightnessLow
in 0.3f..0.6f -> Icons.Default.BrightnessMedium
in 0.6f..1f -> Icons.Default.BrightnessHigh
else -> Icons.Default.BrightnessMedium
},
null
)
}
}


@Composable
fun VolumeSlider(
volume: Int,
range: ClosedRange<Int>
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
volume.toString(),
style = MaterialTheme.typography.bodySmall
)
VerticalSlider(
volume,
range
)
Icon(
when(percentageInt(volume, range)) {
0f -> Icons.AutoMirrored.Default.VolumeOff
in 0f..0.3f -> Icons.AutoMirrored.Default.VolumeMute
in 0.3f..0.6f -> Icons.AutoMirrored.Default.VolumeDown
in 0.6f..1f -> Icons.AutoMirrored.Default.VolumeUp
else -> Icons.AutoMirrored.Default.VolumeOff
},
null
)
}
}

0 comments on commit 19d5ae6

Please sign in to comment.