make brightnes/volume indicators appear and disapear
This commit is contained in:
parent
c27f2685c8
commit
fb28aea8f8
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 = {})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
FullscreenGestureUI(
|
||||
uiVissible = uiVissible,
|
||||
fastSeekSeconds = fastSeekSeconds,
|
||||
hideUi = hideUi,
|
||||
showUi = showUi,
|
||||
fastSeek = fastSeek,
|
||||
brightnes = brightnes,
|
||||
volume = soundVolume,
|
||||
switchToEmbeddedView = switchToEmbeddedView,
|
||||
fastSeekFinished = fastSeekFinished,
|
||||
volumeChange = volumeChange,
|
||||
brightnesChange = brightnesChange)
|
||||
} 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 = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
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(
|
||||
|
|
Loading…
Reference in New Issue