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 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
)
}
}

View File

@ -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) {}

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() {
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")
}

View File

@ -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 = {})
}
}
}

View File

@ -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
)
}
}

View File

@ -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 = {})
}
}
}

View File

@ -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
}
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)
}
EmbeddedGestureUI(
fastSeekSeconds = fastSeekSeconds,
uiVissible = uiVissible,
switchToFullscreen = switchToFullscreen,
embeddedDraggedDownBy = embeddedDraggedDownBy,
fastSeek = fastSeek,
fastSeekFinished = fastSeekFinished,
hideUi = hideUi,
showUi = showUi)
}
}
@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
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(

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 = {},
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

View File

@ -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(