From fb28aea8f8fbc88dec07cf85424cf4cff30612eb Mon Sep 17 00:00:00 2001 From: Christian Schabesberger Date: Thu, 8 Aug 2024 14:26:57 +0200 Subject: [PATCH] make brightnes/volume indicators appear and disapear --- .../newplayer/model/VideoPlayerUIState.kt | 6 +- .../newplayer/model/VideoPlayerViewModel.kt | 2 + .../model/VideoPlayerViewModelImpl.kt | 16 ++ .../newplayer/ui/VideoPlayerControllerUI.kt | 36 ++- .../net/newpipe/newplayer/ui/VideoPlayerUI.kt | 6 +- .../net/newpipe/newplayer/ui/theme/Color.kt | 6 +- .../newplayer/ui/videoplayer/GestureUI.kt | 253 +++--------------- .../gesture_ui/EmbeddedGestureUI.kt | 136 ++++++++++ .../gesture_ui/FastSeekVisualFeedback.kt | 61 ++++- .../gesture_ui/FullscreenGestureUI.kt | 241 +++++++++++++++++ .../ui/videoplayer/gesture_ui/TouchSurface.kt | 2 + .../ui/videoplayer/gesture_ui/VolumeCircle.kt | 19 +- 12 files changed, 543 insertions(+), 241 deletions(-) create mode 100644 new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/EmbeddedGestureUI.kt create mode 100644 new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FullscreenGestureUI.kt diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt index f08fcbd..7f163c1 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerUIState.kt @@ -39,6 +39,8 @@ data class VideoPlayerUIState( val durationInMs: Long, val playbackPositionInMs: Long, val fastseekSeconds: Int, + val soundVolume: Float, + val brightnes: Float ) : Parcelable { companion object { val DEFAULT = VideoPlayerUIState( @@ -54,7 +56,9 @@ data class VideoPlayerUIState( isLoading = true, durationInMs = 0, playbackPositionInMs = 0, - fastseekSeconds = 0 + fastseekSeconds = 0, + soundVolume = 0f, + brightnes = 0f ) } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt index f42b6d8..dd7c4ed 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModel.kt @@ -50,6 +50,8 @@ interface VideoPlayerViewModel { fun embeddedDraggedDown(offset: Float) fun fastSeek(count: Int) fun finishFastSeek() + fun brightnesChange(changeRate: Float) + fun volumeChange(changeRate: Float) interface Listener { fun onFullscreenToggle(isFullscreen: Boolean) {} diff --git a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt index 534f357..11dff10 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/model/VideoPlayerViewModelImpl.kt @@ -291,6 +291,14 @@ class VideoPlayerViewModelImpl @Inject constructor( } } + override fun brightnesChange(changeRate: Float) { + TODO("Not yet implemented") + } + + override fun volumeChange(changeRate: Float) { + TODO("Not yet implemented") + } + override fun switchToEmbeddedView() { callbackListeners.forEach { it?.onFullscreenToggle(false) } uiVisibilityJob?.cancel() @@ -378,6 +386,14 @@ class VideoPlayerViewModelImpl @Inject constructor( println("dummy impl") } + override fun brightnesChange(changeRate: Float) { + println("dummy impl") + } + + override fun volumeChange(changeRate: Float) { + println("dummy impl") + } + override fun pause() { println("dummy pause") } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt index 48980ea..86bce82 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerControllerUI.kt @@ -63,6 +63,8 @@ fun VideoPlayerControllerUI( playbackPositionInMs: Long, bufferedPercentage: Float, fastSeekSeconds: Int, + soundVolume: Float, + brightnes: Float, play: () -> Unit, pause: () -> Unit, prevStream: () -> Unit, @@ -75,7 +77,9 @@ fun VideoPlayerControllerUI( seekingFinished: () -> Unit, embeddedDraggedDownBy: (Float) -> Unit, fastSeek: (Int) -> Unit, - finishFastSeek: () -> Unit + finishFastSeek: () -> Unit, + brightnesChange: (Float) -> Unit, + volumeChange: (Float) -> Unit ) { if (fullscreen) { @@ -99,11 +103,15 @@ fun VideoPlayerControllerUI( uiVissible = uiVissible, fullscreen = fullscreen, fastSeekSeconds = fastSeekSeconds, + brightnes = brightnes, + soundVolume = soundVolume, switchToFullscreen = switchToFullscreen, switchToEmbeddedView = switchToEmbeddedView, embeddedDraggedDownBy = embeddedDraggedDownBy, fastSeek = fastSeek, - fastSeekFinished = finishFastSeek + fastSeekFinished = finishFastSeek, + brightnesChange = brightnesChange, + volumeChange = volumeChange ) } @@ -135,11 +143,15 @@ fun VideoPlayerControllerUI( uiVissible = uiVissible, fullscreen = fullscreen, fastSeekSeconds = fastSeekSeconds, + soundVolume = soundVolume, + brightnes = brightnes, switchToFullscreen = switchToFullscreen, switchToEmbeddedView = switchToEmbeddedView, embeddedDraggedDownBy = embeddedDraggedDownBy, fastSeek = fastSeek, - fastSeekFinished = finishFastSeek + fastSeekFinished = finishFastSeek, + volumeChange = volumeChange, + brightnesChange = brightnesChange ) Box(modifier = Modifier.fillMaxSize()) { @@ -223,6 +235,8 @@ fun VideoPlayerControllerUIPreviewEmbedded() { playbackPositionInMs = 6 * 60 * 1000, bufferedPercentage = 0.4f, fastSeekSeconds = 0, + soundVolume = 0f, + brightnes = 0f, play = {}, pause = {}, prevStream = {}, @@ -235,7 +249,9 @@ fun VideoPlayerControllerUIPreviewEmbedded() { seekingFinished = {}, embeddedDraggedDownBy = {}, fastSeek = {}, - finishFastSeek = {}) + finishFastSeek = {}, + brightnesChange = {}, + volumeChange = {}) } } } @@ -254,6 +270,8 @@ fun VideoPlayerControllerUIPreviewLandscape() { playbackPositionInMs = 6 * 60 * 1000, bufferedPercentage = 0.4f, fastSeekSeconds = 0, + brightnes = 0f, + soundVolume = 0f, play = {}, pause = {}, prevStream = {}, @@ -266,7 +284,9 @@ fun VideoPlayerControllerUIPreviewLandscape() { seekingFinished = {}, embeddedDraggedDownBy = {}, fastSeek = {}, - finishFastSeek = {}) + finishFastSeek = {}, + brightnesChange = {}, + volumeChange = {}) } } } @@ -286,6 +306,8 @@ fun VideoPlayerControllerUIPreviewPortrait() { playbackPositionInMs = 6 * 60 * 1000, bufferedPercentage = 0.4f, fastSeekSeconds = 0, + brightnes = 0f, + soundVolume = 0f, play = {}, pause = {}, prevStream = {}, @@ -298,7 +320,9 @@ fun VideoPlayerControllerUIPreviewPortrait() { seekingFinished = {}, embeddedDraggedDownBy = {}, fastSeek = {}, - finishFastSeek = {}) + finishFastSeek = {}, + brightnesChange = {}, + volumeChange = {}) } } } \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt index 45c1f7a..b63cd90 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/VideoPlayerUI.kt @@ -153,6 +153,8 @@ fun VideoPlayerUI( playbackPositionInMs = uiState.playbackPositionInMs, bufferedPercentage = uiState.bufferedPercentage, fastSeekSeconds = uiState.fastseekSeconds, + brightnes = uiState.brightnes, + soundVolume = uiState.soundVolume, play = viewModel::play, pause = viewModel::pause, prevStream = viewModel::prevStream, @@ -165,7 +167,9 @@ fun VideoPlayerUI( seekingFinished = viewModel::seekingFinished, embeddedDraggedDownBy = viewModel::embeddedDraggedDown, fastSeek = viewModel::fastSeek, - finishFastSeek = viewModel::finishFastSeek + finishFastSeek = viewModel::finishFastSeek, + volumeChange = viewModel::volumeChange, + brightnesChange = viewModel::brightnesChange ) } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt index d1c85c5..6d2b9f0 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/theme/Color.kt @@ -84,6 +84,8 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() { playbackPositionInMs = 6*60*1000, bufferedPercentage = 0.4f, fastSeekSeconds = 10, + brightnes = 0f, + soundVolume = 0f, play = {}, pause = {}, prevStream = {}, @@ -96,7 +98,9 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() { seekingFinished = {}, embeddedDraggedDownBy = {}, fastSeek = {}, - finishFastSeek = {}) + finishFastSeek = {}, + brightnesChange = {}, + volumeChange = {}) } } } diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/GestureUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/GestureUI.kt index 00b43c4..756790d 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/GestureUI.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/GestureUI.kt @@ -40,7 +40,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import net.newpipe.newplayer.ui.theme.VideoPlayerTheme +import net.newpipe.newplayer.ui.videoplayer.gesture_ui.EmbeddedGestureUI import net.newpipe.newplayer.ui.videoplayer.gesture_ui.FastSeekVisualFeedback +import net.newpipe.newplayer.ui.videoplayer.gesture_ui.FullscreenGestureUI +import net.newpipe.newplayer.ui.videoplayer.gesture_ui.FullscreenGestureUIPreview import net.newpipe.newplayer.ui.videoplayer.gesture_ui.TouchSurface import net.newpipe.newplayer.ui.videoplayer.gesture_ui.TouchedPosition @@ -61,11 +64,15 @@ fun GestureUI( uiVissible: Boolean, fullscreen: Boolean, fastSeekSeconds: Int, + brightnes: Float, + soundVolume: Float, switchToFullscreen: () -> Unit, switchToEmbeddedView: () -> Unit, embeddedDraggedDownBy: (Float) -> Unit, fastSeek: (Int) -> Unit, - fastSeekFinished: () -> Unit + fastSeekFinished: () -> Unit, + volumeChange: (Float) -> Unit, + brightnesChange: (Float) -> Unit, ) { val defaultOnRegularTap = { if (uiVissible) { @@ -76,227 +83,27 @@ fun GestureUI( } if (fullscreen) { - Row(modifier = modifier) { - TouchSurface( - modifier = Modifier - .weight(1f), - multitapDurationInMs = FAST_SEEKMODE_DURATION, - onRegularTap = defaultOnRegularTap, - onMultiTap = { - println("multitap ${-it}") - fastSeek(-it) - }, - onMultiTapFinished = fastSeekFinished - ) { - FadedAnimationForSeekFeedback(fastSeekSeconds, backwards = true) { fastSeekSecondsToDisplay -> - Box(modifier = Modifier.fillMaxSize()) { - FastSeekVisualFeedback( - seconds = -fastSeekSecondsToDisplay, - backwards = true, - modifier = Modifier.align(Alignment.CenterEnd) - ) - } - } - } - TouchSurface( - modifier = Modifier - .weight(1f), - onRegularTap = defaultOnRegularTap, - multitapDurationInMs = FAST_SEEKMODE_DURATION, - onMovement = { movement -> - if (0 < movement.y) { - switchToEmbeddedView() - } - } - ) - TouchSurface( - modifier = Modifier - .weight(1f), - onRegularTap = defaultOnRegularTap, - multitapDurationInMs = FAST_SEEKMODE_DURATION, - onMultiTap = fastSeek, - onMultiTapFinished = fastSeekFinished - ) { - FadedAnimationForSeekFeedback(fastSeekSeconds) { fastSeekSecondsToDisplay -> - Box(modifier = Modifier.fillMaxSize()) { - FastSeekVisualFeedback( - modifier = Modifier.align(Alignment.CenterStart), - seconds = fastSeekSecondsToDisplay, - backwards = false - ) - } - } - } - } - } else { // (!fullscreen) - val handleDownwardMovement = { movement: TouchedPosition -> - Log.d(TAG, "${movement.x}:${movement.y}") - if (0 < movement.y) { - embeddedDraggedDownBy(movement.y) - } else { - switchToFullscreen() - } - } - - Row(modifier = modifier) { - TouchSurface( - modifier = Modifier - .weight(1f), - multitapDurationInMs = FAST_SEEKMODE_DURATION, - onRegularTap = defaultOnRegularTap, - onMultiTap = { - fastSeek(-it) - }, - onMultiTapFinished = fastSeekFinished, - onMovement = handleDownwardMovement - ) { - FadedAnimationForSeekFeedback(fastSeekSeconds, backwards = true) { fastSeekSecondsToDisplay -> - Box(modifier = Modifier.fillMaxSize()) { - FastSeekVisualFeedback( - modifier = Modifier.align(Alignment.Center), - seconds = -fastSeekSecondsToDisplay, - backwards = true - ) - } - } - } - TouchSurface( - modifier = Modifier - .weight(1f), - multitapDurationInMs = FAST_SEEKMODE_DURATION, - onRegularTap = defaultOnRegularTap, - onMovement = handleDownwardMovement, - onMultiTap = fastSeek, - onMultiTapFinished = fastSeekFinished - ) { - FadedAnimationForSeekFeedback(fastSeekSeconds) { fastSeekSecondsToDisplay -> - Box(modifier = Modifier.fillMaxSize()) { - FastSeekVisualFeedback( - modifier = Modifier.align(Alignment.Center), - seconds = fastSeekSecondsToDisplay, - backwards = false - ) - } - } - } - } - - } -} - - -@Composable -fun FadedAnimationForSeekFeedback( - fastSeekSeconds: Int, - backwards: Boolean = false, - content: @Composable (fastSeekSecondsToDisplay:Int) -> Unit -) { - - var lastSecondsValue by remember { - mutableStateOf(0) - } - - val vissible = if (backwards) { - fastSeekSeconds < 0 + FullscreenGestureUI( + uiVissible = uiVissible, + fastSeekSeconds = fastSeekSeconds, + hideUi = hideUi, + showUi = showUi, + fastSeek = fastSeek, + brightnes = brightnes, + volume = soundVolume, + switchToEmbeddedView = switchToEmbeddedView, + fastSeekFinished = fastSeekFinished, + volumeChange = volumeChange, + brightnesChange = brightnesChange) } else { - 0 < fastSeekSeconds + EmbeddedGestureUI( + fastSeekSeconds = fastSeekSeconds, + uiVissible = uiVissible, + switchToFullscreen = switchToFullscreen, + embeddedDraggedDownBy = embeddedDraggedDownBy, + fastSeek = fastSeek, + fastSeekFinished = fastSeekFinished, + hideUi = hideUi, + showUi = showUi) } - - val disapearEmediatly = if (backwards) { - 0 < fastSeekSeconds - } else { - fastSeekSeconds < 0 - } - - val valueToDisplay = if(vissible) { - lastSecondsValue = fastSeekSeconds - fastSeekSeconds - } else { - lastSecondsValue - } - - if (!disapearEmediatly) { - AnimatedVisibility( - visible = vissible, - enter = fadeIn(animationSpec = tween(SEEK_ANIMATION_FADE_IN)), - exit = fadeOut( - animationSpec = tween(SEEK_ANIMATION_FADE_OUT) - ) - ) { - content(valueToDisplay) - } - } -} - - -@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") -@Composable -fun FullscreenGestureUIPreview() { - VideoPlayerTheme { - Surface(modifier = Modifier.wrapContentSize(), color = Color.DarkGray) { - GestureUI( - modifier = Modifier, - hideUi = { }, - showUi = { }, - uiVissible = false, - fullscreen = true, - fastSeekSeconds = 0, - switchToFullscreen = { println("switch to fullscreen") }, - switchToEmbeddedView = { println("switch to embedded") }, - embeddedDraggedDownBy = { println("embedded dragged down") }, - fastSeek = { println("fast seek by $it steps") }, - fastSeekFinished = {}) - } - } -} - -@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") -@Composable -fun FullscreenGestureUIPreviewInteractive() { - - var seekSeconds by remember { - mutableStateOf(0) - } - - VideoPlayerTheme { - Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) { - GestureUI( - modifier = Modifier, - hideUi = { }, - showUi = { }, - uiVissible = false, - fullscreen = true, - fastSeekSeconds = seekSeconds, - switchToFullscreen = { println("switch to fullscreen") }, - switchToEmbeddedView = { println("switch to embedded") }, - embeddedDraggedDownBy = { println("embedded dragged down") }, - fastSeek = { seekSeconds = it * 10 }, - fastSeekFinished = { - seekSeconds = 0 - }) - } - } -} - - -@Preview(device = "spec:width=600px,height=400px,dpi=440,orientation=landscape") -@Composable -fun EmbeddedGestureUIPreview() { - VideoPlayerTheme { - Surface(modifier = Modifier.wrapContentSize(), color = Color.DarkGray) { - GestureUI( - modifier = Modifier, - hideUi = { }, - showUi = { }, - uiVissible = false, - fullscreen = false, - fastSeekSeconds = 0, - switchToFullscreen = { println("switch to fullscreen") }, - switchToEmbeddedView = { println("switch to embedded") }, - embeddedDraggedDownBy = { println("embedded dragged down") }, - fastSeek = { println("Fast seek by $it steps") }, - fastSeekFinished = {}) - } - } -} - +} \ No newline at end of file diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/EmbeddedGestureUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/EmbeddedGestureUI.kt new file mode 100644 index 0000000..d180c3a --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/EmbeddedGestureUI.kt @@ -0,0 +1,136 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + + +package net.newpipe.newplayer.ui.videoplayer.gesture_ui + +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import net.newpipe.newplayer.ui.theme.VideoPlayerTheme +import net.newpipe.newplayer.ui.videoplayer.FAST_SEEKMODE_DURATION + +private const val TAG = "EmbeddedGestureUI" + +@Composable +fun EmbeddedGestureUI( + modifier: Modifier = Modifier, + fastSeekSeconds: Int, + uiVissible: Boolean, + switchToFullscreen: () -> Unit, + embeddedDraggedDownBy: (Float) -> Unit, + fastSeek: (Int) -> Unit, + fastSeekFinished: () -> Unit, + hideUi: () -> Unit, + showUi: () -> Unit +) { + + val handleDownwardMovement = { movement: TouchedPosition -> + Log.d(TAG, "${movement.x}:${movement.y}") + if (0 < movement.y) { + embeddedDraggedDownBy(movement.y) + } else { + switchToFullscreen() + } + } + + val defaultOnRegularTap = { + if (uiVissible) { + hideUi() + } else { + showUi() + } + } + + Row(modifier = modifier) { + TouchSurface( + modifier = Modifier + .weight(1f), + multitapDurationInMs = FAST_SEEKMODE_DURATION, + onRegularTap = defaultOnRegularTap, + onMultiTap = { + fastSeek(-it) + }, + onMultiTapFinished = fastSeekFinished, + onMovement = handleDownwardMovement + ) { + FadedAnimationForSeekFeedback( + fastSeekSeconds, + backwards = true + ) { fastSeekSecondsToDisplay -> + Box(modifier = Modifier.fillMaxSize()) { + FastSeekVisualFeedback( + modifier = Modifier.align(Alignment.Center), + seconds = -fastSeekSecondsToDisplay, + backwards = true + ) + } + } + } + TouchSurface( + modifier = Modifier + .weight(1f), + multitapDurationInMs = FAST_SEEKMODE_DURATION, + onRegularTap = defaultOnRegularTap, + onMovement = handleDownwardMovement, + onMultiTap = fastSeek, + onMultiTapFinished = fastSeekFinished + ) { + FadedAnimationForSeekFeedback(fastSeekSeconds) { fastSeekSecondsToDisplay -> + Box(modifier = Modifier.fillMaxSize()) { + FastSeekVisualFeedback( + modifier = Modifier.align(Alignment.Center), + seconds = fastSeekSecondsToDisplay, + backwards = false + ) + } + } + } + } +} + + +@Preview(device = "spec:width=600px,height=400px,dpi=440,orientation=landscape") +@Composable +fun EmbeddedGestureUIPreview() { + VideoPlayerTheme { + Surface(modifier = Modifier.wrapContentSize(), color = Color.DarkGray) { + EmbeddedGestureUI( + modifier = Modifier, + hideUi = { }, + showUi = { }, + uiVissible = false, + fastSeekSeconds = 0, + switchToFullscreen = { println("switch to fullscreen") }, + embeddedDraggedDownBy = { println("embedded dragged down") }, + fastSeek = { println("Fast seek by $it steps") }, + fastSeekFinished = {}) + } + } +} + diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FastSeekVisualFeedback.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FastSeekVisualFeedback.kt index 0679349..a97630d 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FastSeekVisualFeedback.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FastSeekVisualFeedback.kt @@ -20,12 +20,16 @@ package net.newpipe.newplayer.ui.videoplayer.gesture_ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColor import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -37,29 +41,35 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.google.android.material.color.MaterialColors import net.newpipe.newplayer.R import net.newpipe.newplayer.ui.theme.VideoPlayerTheme import net.newpipe.newplayer.ui.videoplayer.SEEK_ANIMATION_DURATION_IN_MS +import net.newpipe.newplayer.ui.videoplayer.SEEK_ANIMATION_FADE_IN +import net.newpipe.newplayer.ui.videoplayer.SEEK_ANIMATION_FADE_OUT @Composable fun FastSeekVisualFeedback(modifier: Modifier = Modifier, seconds: Int, backwards: Boolean) { val contentDescription = String.format( if (backwards) { - "Fast seeking backward by %d seconds." - //stringResource(id = R.string.fast_seeking_backward) + //"Fast seeking backward by %d seconds." + stringResource(id = R.string.fast_seeking_backward) } else { - "Fast seeking forward by %d seconds." - //stringResource(id = R.string.fast_seeking_forward) + //"Fast seeking forward by %d seconds." + stringResource(id = R.string.fast_seeking_forward) }, seconds ) @@ -142,6 +152,49 @@ fun FastSeekVisualFeedback(modifier: Modifier = Modifier, seconds: Int, backward } +@Composable +fun FadedAnimationForSeekFeedback( + fastSeekSeconds: Int, + backwards: Boolean = false, + content: @Composable (fastSeekSecondsToDisplay:Int) -> Unit +) { + + var lastSecondsValue by remember { + mutableStateOf(0) + } + + val vissible = if (backwards) { + fastSeekSeconds < 0 + } else { + 0 < fastSeekSeconds + } + + val disapearEmediatly = if (backwards) { + 0 < fastSeekSeconds + } else { + fastSeekSeconds < 0 + } + + val valueToDisplay = if(vissible) { + lastSecondsValue = fastSeekSeconds + fastSeekSeconds + } else { + lastSecondsValue + } + + if (!disapearEmediatly) { + AnimatedVisibility( + visible = vissible, + enter = fadeIn(animationSpec = tween(SEEK_ANIMATION_FADE_IN)), + exit = fadeOut( + animationSpec = tween(SEEK_ANIMATION_FADE_OUT) + ) + ) { + content(valueToDisplay) + } + } +} + @Composable private fun SeekerIcon(backwards: Boolean, description: String, color: Color) { Icon( diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FullscreenGestureUI.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FullscreenGestureUI.kt new file mode 100644 index 0000000..173cb04 --- /dev/null +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/FullscreenGestureUI.kt @@ -0,0 +1,241 @@ +/* NewPlayer + * + * @author Christian Schabesberger + * + * Copyright (C) NewPipe e.V. 2024 + * + * NewPlayer is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * NewPlayer is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with NewPlayer. If not, see . + */ + + +package net.newpipe.newplayer.ui.videoplayer.gesture_ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.tooling.preview.Preview +import net.newpipe.newplayer.ui.theme.VideoPlayerTheme +import net.newpipe.newplayer.ui.videoplayer.FAST_SEEKMODE_DURATION + +@Composable +fun FullscreenGestureUI( + modifier: Modifier = Modifier, + uiVissible: Boolean, + fastSeekSeconds: Int, + volume: Float, + brightnes: Float, + hideUi: () -> Unit, + showUi: () -> Unit, + fastSeek: (Int) -> Unit, + fastSeekFinished: () -> Unit, + switchToEmbeddedView: () -> Unit, + volumeChange: (Float) -> Unit, + brightnesChange: (Float) -> Unit +) { + val defaultOnRegularTap = { + if (uiVissible) { + hideUi() + } else { + showUi() + } + } + + var heightPx by remember { + mutableStateOf(0f) + } + + var volumeIndicatorVissible by remember { + mutableStateOf(false) + } + + var brightnesIndicatorVissible by remember { + mutableStateOf(false) + } + + Box(modifier = modifier.onGloballyPositioned { coordinates -> + heightPx = coordinates.size.height.toFloat() + }) { + Row { + TouchSurface( + modifier = Modifier + .weight(1f), + multitapDurationInMs = FAST_SEEKMODE_DURATION, + onRegularTap = defaultOnRegularTap, + onMultiTap = { + println("multitap ${-it}") + fastSeek(-it) + }, + onMultiTapFinished = fastSeekFinished, + onUp = { + brightnesIndicatorVissible = false + }, + onMovement = {change -> + brightnesIndicatorVissible = true + if (heightPx != 0f) { + brightnesChange(-change.y / heightPx) + } + } + ) { + FadedAnimationForSeekFeedback( + fastSeekSeconds, + backwards = true + ) { fastSeekSecondsToDisplay -> + Box(modifier = Modifier.fillMaxSize()) { + FastSeekVisualFeedback( + seconds = -fastSeekSecondsToDisplay, + backwards = true, + modifier = Modifier.align(Alignment.CenterEnd) + ) + } + } + } + TouchSurface( + modifier = Modifier + .weight(1f), + onRegularTap = defaultOnRegularTap, + multitapDurationInMs = FAST_SEEKMODE_DURATION, + onMovement = { movement -> + if (0 < movement.y) { + switchToEmbeddedView() + } + } + ) + TouchSurface( + modifier = Modifier + .weight(1f), + onRegularTap = defaultOnRegularTap, + multitapDurationInMs = FAST_SEEKMODE_DURATION, + onMultiTap = fastSeek, + onMultiTapFinished = fastSeekFinished, + onUp = { + volumeIndicatorVissible = false + }, + onMovement = { change -> + volumeIndicatorVissible = true + if (heightPx != 0f) { + volumeChange(-change.y / heightPx) + } + } + ) { + FadedAnimationForSeekFeedback(fastSeekSeconds) { fastSeekSecondsToDisplay -> + Box(modifier = Modifier.fillMaxSize()) { + FastSeekVisualFeedback( + modifier = Modifier.align(Alignment.CenterStart), + seconds = fastSeekSecondsToDisplay, + backwards = false + ) + } + } + } + } + AnimatedVisibility(modifier = Modifier.align(Alignment.Center), + visible = volumeIndicatorVissible, + enter = scaleIn(initialScale = 0.95f, animationSpec = tween(100)), + exit = scaleOut(targetScale = 0.95f, animationSpec = tween(100))) { + VolumeCircle(volumeFraction = volume) + } + + AnimatedVisibility(modifier = Modifier.align(Alignment.Center), + visible = brightnesIndicatorVissible, + enter = scaleIn(initialScale = 0.95f, animationSpec = tween(100)), + exit = scaleOut(targetScale = 0.95f, animationSpec = tween(100))) { + VolumeCircle( + volumeFraction = brightnes, + modifier = Modifier.align(Alignment.Center), + isBrightnes = true + ) + } + } +} + + +@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") +@Composable +fun FullscreenGestureUIPreview() { + VideoPlayerTheme { + Surface(modifier = Modifier.wrapContentSize(), color = Color.DarkGray) { + FullscreenGestureUI( + modifier = Modifier, + hideUi = { }, + showUi = { }, + uiVissible = false, + fastSeekSeconds = 0, + volume = 0f, + brightnes = 0f, + fastSeek = { println("fast seek by $it steps") }, + fastSeekFinished = {}, + switchToEmbeddedView = {}, + brightnesChange = {}, + volumeChange = {}) + } + } +} + +@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape") +@Composable +fun FullscreenGestureUIPreviewInteractive() { + + var seekSeconds by remember { + mutableStateOf(0) + } + + var brightnesValue by remember { + mutableStateOf(0f) + } + + var soundVolume by remember { + mutableStateOf(0f) + } + + VideoPlayerTheme { + Surface(modifier = Modifier.wrapContentSize(), color = Color.Gray) { + FullscreenGestureUI( + modifier = Modifier, + hideUi = { }, + showUi = { }, + uiVissible = false, + fastSeekSeconds = seekSeconds, + volume = soundVolume, + brightnes = brightnesValue, + fastSeek = { seekSeconds = it * 10 }, + fastSeekFinished = { + seekSeconds = 0 + }, + switchToEmbeddedView = {}, + brightnesChange = { + brightnesValue = (brightnesValue + it).coerceIn(0f, 1f) + }, + volumeChange = { + soundVolume = (soundVolume + it).coerceIn(0f, 1f) + }) + } + } +} diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/TouchSurface.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/TouchSurface.kt index cef4607..6ab4a7d 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/TouchSurface.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/TouchSurface.kt @@ -47,6 +47,7 @@ fun TouchSurface( onMultiTap: (Int) -> Unit = {}, onMultiTapFinished: () -> Unit = {}, onRegularTap: () -> Unit = {}, + onUp: () -> Unit = {}, onMovement: (TouchedPosition) -> Unit = {}, content: @Composable () -> Unit = {} ) { @@ -82,6 +83,7 @@ fun TouchSurface( } val defaultActionUp = { onMultiTap: (Int) -> Unit, onRegularTap: () -> Unit -> + onUp() val currentTime = System.currentTimeMillis() if (!moveOccured) { val timeSinceLastTouch = currentTime - lastTouchTime diff --git a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/VolumeCircle.kt b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/VolumeCircle.kt index 4e50cc8..c89893f 100644 --- a/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/VolumeCircle.kt +++ b/new-player/src/main/java/net/newpipe/newplayer/ui/videoplayer/gesture_ui/VolumeCircle.kt @@ -57,12 +57,19 @@ private const val LINE_STROKE_WIDTH = 4 private const val CIRCLE_SIZE = 100 @Composable -fun VolumeCircle(modifier: Modifier = Modifier, volumeFraction: Float, isBrightnes: Boolean = false) { - assert(0f < volumeFraction && volumeFraction < 1f) { - Log.e(TAG, "Volume fraction must be in ragne [0;1]. It was $volumeFraction") +fun VolumeCircle( + modifier: Modifier = Modifier, + volumeFraction: Float, + isBrightnes: Boolean = false +) { + assert(0f <= volumeFraction && volumeFraction <= 1f) { + Log.e(TAG, "Volume fraction must be in ragne [0;1]. It was $volumeFraction") } - Box(modifier.shadow(elevation = 1.dp, shape = CircleShape).padding(2.dp)){ + Box( + modifier + .shadow(elevation = 1.dp, shape = CircleShape) + .padding(2.dp)) { Canvas(Modifier.size(CIRCLE_SIZE.dp)) { val arcSize = (CIRCLE_SIZE - LINE_STROKE_WIDTH).dp.toPx(); drawCircle(color = Color.Black.copy(alpha = 0.3f), radius = (CIRCLE_SIZE / 2).dp.toPx()) @@ -80,7 +87,9 @@ fun VolumeCircle(modifier: Modifier = Modifier, volumeFraction: Float, isBrightn } Icon( - modifier = Modifier.align(Alignment.Center).size(60.dp), + modifier = Modifier + .align(Alignment.Center) + .size(60.dp), imageVector = (if (isBrightnes) getBrightnesIcon(volumeFraction = volumeFraction) else getVolumeIcon(volumeFraction = volumeFraction)), contentDescription = stringResource(