make brightnes/volume indicators appear and disapear

This commit is contained in:
Christian Schabesberger 2024-08-08 14:26:57 +02:00
parent c27f2685c8
commit fb28aea8f8
12 changed files with 543 additions and 241 deletions

View File

@ -39,6 +39,8 @@ data class VideoPlayerUIState(
val durationInMs: Long, val durationInMs: Long,
val playbackPositionInMs: Long, val playbackPositionInMs: Long,
val fastseekSeconds: Int, val fastseekSeconds: Int,
val soundVolume: Float,
val brightnes: Float
) : Parcelable { ) : Parcelable {
companion object { companion object {
val DEFAULT = VideoPlayerUIState( val DEFAULT = VideoPlayerUIState(
@ -54,7 +56,9 @@ data class VideoPlayerUIState(
isLoading = true, isLoading = true,
durationInMs = 0, durationInMs = 0,
playbackPositionInMs = 0, playbackPositionInMs = 0,
fastseekSeconds = 0 fastseekSeconds = 0,
soundVolume = 0f,
brightnes = 0f
) )
} }
} }

View File

@ -50,6 +50,8 @@ interface VideoPlayerViewModel {
fun embeddedDraggedDown(offset: Float) fun embeddedDraggedDown(offset: Float)
fun fastSeek(count: Int) fun fastSeek(count: Int)
fun finishFastSeek() fun finishFastSeek()
fun brightnesChange(changeRate: Float)
fun volumeChange(changeRate: Float)
interface Listener { interface Listener {
fun onFullscreenToggle(isFullscreen: Boolean) {} fun onFullscreenToggle(isFullscreen: Boolean) {}

View File

@ -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() { override fun switchToEmbeddedView() {
callbackListeners.forEach { it?.onFullscreenToggle(false) } callbackListeners.forEach { it?.onFullscreenToggle(false) }
uiVisibilityJob?.cancel() uiVisibilityJob?.cancel()
@ -378,6 +386,14 @@ class VideoPlayerViewModelImpl @Inject constructor(
println("dummy impl") println("dummy impl")
} }
override fun brightnesChange(changeRate: Float) {
println("dummy impl")
}
override fun volumeChange(changeRate: Float) {
println("dummy impl")
}
override fun pause() { override fun pause() {
println("dummy pause") println("dummy pause")
} }

View File

@ -63,6 +63,8 @@ fun VideoPlayerControllerUI(
playbackPositionInMs: Long, playbackPositionInMs: Long,
bufferedPercentage: Float, bufferedPercentage: Float,
fastSeekSeconds: Int, fastSeekSeconds: Int,
soundVolume: Float,
brightnes: Float,
play: () -> Unit, play: () -> Unit,
pause: () -> Unit, pause: () -> Unit,
prevStream: () -> Unit, prevStream: () -> Unit,
@ -75,7 +77,9 @@ fun VideoPlayerControllerUI(
seekingFinished: () -> Unit, seekingFinished: () -> Unit,
embeddedDraggedDownBy: (Float) -> Unit, embeddedDraggedDownBy: (Float) -> Unit,
fastSeek: (Int) -> Unit, fastSeek: (Int) -> Unit,
finishFastSeek: () -> Unit finishFastSeek: () -> Unit,
brightnesChange: (Float) -> Unit,
volumeChange: (Float) -> Unit
) { ) {
if (fullscreen) { if (fullscreen) {
@ -99,11 +103,15 @@ fun VideoPlayerControllerUI(
uiVissible = uiVissible, uiVissible = uiVissible,
fullscreen = fullscreen, fullscreen = fullscreen,
fastSeekSeconds = fastSeekSeconds, fastSeekSeconds = fastSeekSeconds,
brightnes = brightnes,
soundVolume = soundVolume,
switchToFullscreen = switchToFullscreen, switchToFullscreen = switchToFullscreen,
switchToEmbeddedView = switchToEmbeddedView, switchToEmbeddedView = switchToEmbeddedView,
embeddedDraggedDownBy = embeddedDraggedDownBy, embeddedDraggedDownBy = embeddedDraggedDownBy,
fastSeek = fastSeek, fastSeek = fastSeek,
fastSeekFinished = finishFastSeek fastSeekFinished = finishFastSeek,
brightnesChange = brightnesChange,
volumeChange = volumeChange
) )
} }
@ -135,11 +143,15 @@ fun VideoPlayerControllerUI(
uiVissible = uiVissible, uiVissible = uiVissible,
fullscreen = fullscreen, fullscreen = fullscreen,
fastSeekSeconds = fastSeekSeconds, fastSeekSeconds = fastSeekSeconds,
soundVolume = soundVolume,
brightnes = brightnes,
switchToFullscreen = switchToFullscreen, switchToFullscreen = switchToFullscreen,
switchToEmbeddedView = switchToEmbeddedView, switchToEmbeddedView = switchToEmbeddedView,
embeddedDraggedDownBy = embeddedDraggedDownBy, embeddedDraggedDownBy = embeddedDraggedDownBy,
fastSeek = fastSeek, fastSeek = fastSeek,
fastSeekFinished = finishFastSeek fastSeekFinished = finishFastSeek,
volumeChange = volumeChange,
brightnesChange = brightnesChange
) )
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@ -223,6 +235,8 @@ fun VideoPlayerControllerUIPreviewEmbedded() {
playbackPositionInMs = 6 * 60 * 1000, playbackPositionInMs = 6 * 60 * 1000,
bufferedPercentage = 0.4f, bufferedPercentage = 0.4f,
fastSeekSeconds = 0, fastSeekSeconds = 0,
soundVolume = 0f,
brightnes = 0f,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},
@ -235,7 +249,9 @@ fun VideoPlayerControllerUIPreviewEmbedded() {
seekingFinished = {}, seekingFinished = {},
embeddedDraggedDownBy = {}, embeddedDraggedDownBy = {},
fastSeek = {}, fastSeek = {},
finishFastSeek = {}) finishFastSeek = {},
brightnesChange = {},
volumeChange = {})
} }
} }
} }
@ -254,6 +270,8 @@ fun VideoPlayerControllerUIPreviewLandscape() {
playbackPositionInMs = 6 * 60 * 1000, playbackPositionInMs = 6 * 60 * 1000,
bufferedPercentage = 0.4f, bufferedPercentage = 0.4f,
fastSeekSeconds = 0, fastSeekSeconds = 0,
brightnes = 0f,
soundVolume = 0f,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},
@ -266,7 +284,9 @@ fun VideoPlayerControllerUIPreviewLandscape() {
seekingFinished = {}, seekingFinished = {},
embeddedDraggedDownBy = {}, embeddedDraggedDownBy = {},
fastSeek = {}, fastSeek = {},
finishFastSeek = {}) finishFastSeek = {},
brightnesChange = {},
volumeChange = {})
} }
} }
} }
@ -286,6 +306,8 @@ fun VideoPlayerControllerUIPreviewPortrait() {
playbackPositionInMs = 6 * 60 * 1000, playbackPositionInMs = 6 * 60 * 1000,
bufferedPercentage = 0.4f, bufferedPercentage = 0.4f,
fastSeekSeconds = 0, fastSeekSeconds = 0,
brightnes = 0f,
soundVolume = 0f,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},
@ -298,7 +320,9 @@ fun VideoPlayerControllerUIPreviewPortrait() {
seekingFinished = {}, seekingFinished = {},
embeddedDraggedDownBy = {}, embeddedDraggedDownBy = {},
fastSeek = {}, fastSeek = {},
finishFastSeek = {}) finishFastSeek = {},
brightnesChange = {},
volumeChange = {})
} }
} }
} }

View File

@ -153,6 +153,8 @@ fun VideoPlayerUI(
playbackPositionInMs = uiState.playbackPositionInMs, playbackPositionInMs = uiState.playbackPositionInMs,
bufferedPercentage = uiState.bufferedPercentage, bufferedPercentage = uiState.bufferedPercentage,
fastSeekSeconds = uiState.fastseekSeconds, fastSeekSeconds = uiState.fastseekSeconds,
brightnes = uiState.brightnes,
soundVolume = uiState.soundVolume,
play = viewModel::play, play = viewModel::play,
pause = viewModel::pause, pause = viewModel::pause,
prevStream = viewModel::prevStream, prevStream = viewModel::prevStream,
@ -165,7 +167,9 @@ fun VideoPlayerUI(
seekingFinished = viewModel::seekingFinished, seekingFinished = viewModel::seekingFinished,
embeddedDraggedDownBy = viewModel::embeddedDraggedDown, embeddedDraggedDownBy = viewModel::embeddedDraggedDown,
fastSeek = viewModel::fastSeek, fastSeek = viewModel::fastSeek,
finishFastSeek = viewModel::finishFastSeek finishFastSeek = viewModel::finishFastSeek,
volumeChange = viewModel::volumeChange,
brightnesChange = viewModel::brightnesChange
) )
} }
} }

View File

@ -84,6 +84,8 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() {
playbackPositionInMs = 6*60*1000, playbackPositionInMs = 6*60*1000,
bufferedPercentage = 0.4f, bufferedPercentage = 0.4f,
fastSeekSeconds = 10, fastSeekSeconds = 10,
brightnes = 0f,
soundVolume = 0f,
play = {}, play = {},
pause = {}, pause = {},
prevStream = {}, prevStream = {},
@ -96,7 +98,9 @@ fun VideoPlayerControllerUIPreviewEmbeddedColorPreview() {
seekingFinished = {}, seekingFinished = {},
embeddedDraggedDownBy = {}, embeddedDraggedDownBy = {},
fastSeek = {}, fastSeek = {},
finishFastSeek = {}) finishFastSeek = {},
brightnesChange = {},
volumeChange = {})
} }
} }
} }

View File

@ -40,7 +40,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 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.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.TouchSurface
import net.newpipe.newplayer.ui.videoplayer.gesture_ui.TouchedPosition import net.newpipe.newplayer.ui.videoplayer.gesture_ui.TouchedPosition
@ -61,11 +64,15 @@ fun GestureUI(
uiVissible: Boolean, uiVissible: Boolean,
fullscreen: Boolean, fullscreen: Boolean,
fastSeekSeconds: Int, fastSeekSeconds: Int,
brightnes: Float,
soundVolume: Float,
switchToFullscreen: () -> Unit, switchToFullscreen: () -> Unit,
switchToEmbeddedView: () -> Unit, switchToEmbeddedView: () -> Unit,
embeddedDraggedDownBy: (Float) -> Unit, embeddedDraggedDownBy: (Float) -> Unit,
fastSeek: (Int) -> Unit, fastSeek: (Int) -> Unit,
fastSeekFinished: () -> Unit fastSeekFinished: () -> Unit,
volumeChange: (Float) -> Unit,
brightnesChange: (Float) -> Unit,
) { ) {
val defaultOnRegularTap = { val defaultOnRegularTap = {
if (uiVissible) { if (uiVissible) {
@ -76,227 +83,27 @@ fun GestureUI(
} }
if (fullscreen) { if (fullscreen) {
Row(modifier = modifier) { FullscreenGestureUI(
TouchSurface( uiVissible = uiVissible,
modifier = Modifier fastSeekSeconds = fastSeekSeconds,
.weight(1f), hideUi = hideUi,
multitapDurationInMs = FAST_SEEKMODE_DURATION, showUi = showUi,
onRegularTap = defaultOnRegularTap, fastSeek = fastSeek,
onMultiTap = { brightnes = brightnes,
println("multitap ${-it}") volume = soundVolume,
fastSeek(-it) switchToEmbeddedView = switchToEmbeddedView,
}, fastSeekFinished = fastSeekFinished,
onMultiTapFinished = fastSeekFinished volumeChange = volumeChange,
) { brightnesChange = brightnesChange)
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 { } else {
switchToFullscreen() EmbeddedGestureUI(
fastSeekSeconds = fastSeekSeconds,
uiVissible = uiVissible,
switchToFullscreen = switchToFullscreen,
embeddedDraggedDownBy = embeddedDraggedDownBy,
fastSeek = fastSeek,
fastSeekFinished = fastSeekFinished,
hideUi = hideUi,
showUi = 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
)
}
}
}
}
}
}
@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)
}
}
}
@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 = {})
}
}
}

View File

@ -0,0 +1,136 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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 = {})
}
}
}

View File

@ -20,12 +20,16 @@
package net.newpipe.newplayer.ui.videoplayer.gesture_ui package net.newpipe.newplayer.ui.videoplayer.gesture_ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColor import androidx.compose.animation.animateColor
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.keyframes import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberInfiniteTransition 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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -37,29 +41,35 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue 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.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.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import net.newpipe.newplayer.R import net.newpipe.newplayer.R
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme 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_DURATION_IN_MS
import net.newpipe.newplayer.ui.videoplayer.SEEK_ANIMATION_FADE_IN
import net.newpipe.newplayer.ui.videoplayer.SEEK_ANIMATION_FADE_OUT
@Composable @Composable
fun FastSeekVisualFeedback(modifier: Modifier = Modifier, seconds: Int, backwards: Boolean) { fun FastSeekVisualFeedback(modifier: Modifier = Modifier, seconds: Int, backwards: Boolean) {
val contentDescription = String.format( val contentDescription = String.format(
if (backwards) { if (backwards) {
"Fast seeking backward by %d seconds." //"Fast seeking backward by %d seconds."
//stringResource(id = R.string.fast_seeking_backward) stringResource(id = R.string.fast_seeking_backward)
} else { } else {
"Fast seeking forward by %d seconds." //"Fast seeking forward by %d seconds."
//stringResource(id = R.string.fast_seeking_forward) stringResource(id = R.string.fast_seeking_forward)
}, seconds }, 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 @Composable
private fun SeekerIcon(backwards: Boolean, description: String, color: Color) { private fun SeekerIcon(backwards: Boolean, description: String, color: Color) {
Icon( Icon(

View File

@ -0,0 +1,241 @@
/* NewPlayer
*
* @author Christian Schabesberger
*
* Copyright (C) NewPipe e.V. 2024 <code(at)newpipe-ev.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
})
}
}
}

View File

@ -47,6 +47,7 @@ fun TouchSurface(
onMultiTap: (Int) -> Unit = {}, onMultiTap: (Int) -> Unit = {},
onMultiTapFinished: () -> Unit = {}, onMultiTapFinished: () -> Unit = {},
onRegularTap: () -> Unit = {}, onRegularTap: () -> Unit = {},
onUp: () -> Unit = {},
onMovement: (TouchedPosition) -> Unit = {}, onMovement: (TouchedPosition) -> Unit = {},
content: @Composable () -> Unit = {} content: @Composable () -> Unit = {}
) { ) {
@ -82,6 +83,7 @@ fun TouchSurface(
} }
val defaultActionUp = { onMultiTap: (Int) -> Unit, onRegularTap: () -> Unit -> val defaultActionUp = { onMultiTap: (Int) -> Unit, onRegularTap: () -> Unit ->
onUp()
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (!moveOccured) { if (!moveOccured) {
val timeSinceLastTouch = currentTime - lastTouchTime val timeSinceLastTouch = currentTime - lastTouchTime

View File

@ -57,12 +57,19 @@ private const val LINE_STROKE_WIDTH = 4
private const val CIRCLE_SIZE = 100 private const val CIRCLE_SIZE = 100
@Composable @Composable
fun VolumeCircle(modifier: Modifier = Modifier, volumeFraction: Float, isBrightnes: Boolean = false) { fun VolumeCircle(
assert(0f < volumeFraction && volumeFraction < 1f) { 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") 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)) { Canvas(Modifier.size(CIRCLE_SIZE.dp)) {
val arcSize = (CIRCLE_SIZE - LINE_STROKE_WIDTH).dp.toPx(); val arcSize = (CIRCLE_SIZE - LINE_STROKE_WIDTH).dp.toPx();
drawCircle(color = Color.Black.copy(alpha = 0.3f), radius = (CIRCLE_SIZE / 2).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( Icon(
modifier = Modifier.align(Alignment.Center).size(60.dp), modifier = Modifier
.align(Alignment.Center)
.size(60.dp),
imageVector = (if (isBrightnes) getBrightnesIcon(volumeFraction = volumeFraction) imageVector = (if (isBrightnes) getBrightnesIcon(volumeFraction = volumeFraction)
else getVolumeIcon(volumeFraction = volumeFraction)), else getVolumeIcon(volumeFraction = volumeFraction)),
contentDescription = stringResource( contentDescription = stringResource(