rearange gesture UI code
This commit is contained in:
parent
990a4aaa12
commit
cc4dfe7721
|
@ -52,8 +52,8 @@ interface NewPlayer {
|
||||||
fun prepare()
|
fun prepare()
|
||||||
fun play()
|
fun play()
|
||||||
fun pause()
|
fun pause()
|
||||||
fun fastSeekForward()
|
fun fastSeekForward(ntimes: Int = 1)
|
||||||
fun fastSeekBackward()
|
fun fastSeekBackward(ntimes: Int = 1)
|
||||||
fun seekTo(millisecond: Long)
|
fun seekTo(millisecond: Long)
|
||||||
fun addToPlaylist(newItem: String)
|
fun addToPlaylist(newItem: String)
|
||||||
fun addListener(callbackListener: Listener)
|
fun addListener(callbackListener: Listener)
|
||||||
|
@ -115,14 +115,14 @@ class NewPlayerImpl(override val internal_player: Player, override val repositor
|
||||||
internal_player.pause()
|
internal_player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fastSeekForward() {
|
override fun fastSeekForward(ntimes: Int) {
|
||||||
val currentPosition = internal_player.currentPosition
|
val currentPosition = internal_player.currentPosition
|
||||||
internal_player.seekTo(currentPosition + fastSeekAmountSec * 1000)
|
internal_player.seekTo(currentPosition + fastSeekAmountSec * 1000 * ntimes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fastSeekBackward() {
|
override fun fastSeekBackward(ntimes: Int) {
|
||||||
val currentPosition = internal_player.currentPosition
|
val currentPosition = internal_player.currentPosition
|
||||||
internal_player.seekTo(currentPosition - fastSeekAmountSec * 1000)
|
internal_player.seekTo(currentPosition - fastSeekAmountSec * 1000 * ntimes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(millisecond: Long) {
|
override fun seekTo(millisecond: Long) {
|
||||||
|
|
|
@ -59,16 +59,17 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
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.gesture_ui.FastSeekVisualFeedback
|
||||||
|
import net.newpipe.newplayer.ui.videoplayer.gesture_ui.TouchSurface
|
||||||
|
import net.newpipe.newplayer.ui.videoplayer.gesture_ui.TouchedPosition
|
||||||
|
|
||||||
private const val TAG = "TouchUi"
|
private const val TAG = "TouchUi"
|
||||||
|
|
||||||
private data class TouchedPosition(val x: Float, val y: Float) {
|
|
||||||
operator fun minus(other: TouchedPosition) = TouchedPosition(this.x - other.x, this.y - other.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
const val DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS: Long = 200
|
const val DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS: Long = 200
|
||||||
const val SEEK_ANIMATION_DURATION_IN_MS = 400
|
const val SEEK_ANIMATION_DURATION_IN_MS = 400
|
||||||
const val SEEK_ANIMATION_VISSIBLE_IN_MS = 500L
|
const val FAST_SEEKMODE_DURATION = 500L
|
||||||
const val SEEK_ANIMATION_FADE_IN = 200
|
const val SEEK_ANIMATION_FADE_IN = 200
|
||||||
const val SEEK_ANIMATION_FADE_OUT = 500
|
const val SEEK_ANIMATION_FADE_OUT = 500
|
||||||
|
|
||||||
|
@ -94,34 +95,39 @@ fun GestureUI(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var showFastSeekBack by remember {
|
var fastSeekModeBackward by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
var showFastSeekForward by remember {
|
var fastSeekModeForward by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val composeScope = rememberCoroutineScope()
|
val composeScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val doForwardSeek = {
|
val doForwardSeek = {
|
||||||
showFastSeekForward = true
|
fastSeekModeForward = true
|
||||||
composeScope.launch {
|
composeScope.launch {
|
||||||
delay(SEEK_ANIMATION_VISSIBLE_IN_MS)
|
delay(FAST_SEEKMODE_DURATION)
|
||||||
showFastSeekForward = false
|
fastSeekModeForward = false
|
||||||
}
|
}
|
||||||
fastSeekForward()
|
fastSeekForward()
|
||||||
}
|
}
|
||||||
|
|
||||||
val doBackwardSeek = {
|
var fastSeekModeTimeout: Job? = null
|
||||||
showFastSeekBack = true
|
val resetFastSeekModeEnd = {
|
||||||
composeScope.launch {
|
fastSeekModeTimeout?.cancel()
|
||||||
delay(SEEK_ANIMATION_VISSIBLE_IN_MS)
|
fastSeekModeTimeout = composeScope.launch {
|
||||||
showFastSeekBack = false
|
delay(FAST_SEEKMODE_DURATION)
|
||||||
|
fastSeekModeBackward = false
|
||||||
}
|
}
|
||||||
fastSeekBackward()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val doBackwardSeek = {
|
||||||
|
fastSeekModeBackward = true
|
||||||
|
resetFastSeekModeEnd()
|
||||||
|
fastSeekBackward()
|
||||||
|
}
|
||||||
|
|
||||||
if (fullscreen) {
|
if (fullscreen) {
|
||||||
Row(modifier = modifier) {
|
Row(modifier = modifier) {
|
||||||
|
@ -131,7 +137,7 @@ fun GestureUI(
|
||||||
onRegularTap = defaultOnRegularTap,
|
onRegularTap = defaultOnRegularTap,
|
||||||
onDoubleTab = doBackwardSeek
|
onDoubleTab = doBackwardSeek
|
||||||
) {
|
) {
|
||||||
FadedAnimationForSeekFeedback(visible = showFastSeekBack) {
|
FadedAnimationForSeekFeedback(visible = fastSeekModeBackward) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
FastSeekVisualFeedback(
|
FastSeekVisualFeedback(
|
||||||
seconds = fastSeekSeconds,
|
seconds = fastSeekSeconds,
|
||||||
|
@ -157,7 +163,7 @@ fun GestureUI(
|
||||||
onRegularTap = defaultOnRegularTap,
|
onRegularTap = defaultOnRegularTap,
|
||||||
onDoubleTab = doForwardSeek
|
onDoubleTab = doForwardSeek
|
||||||
) {
|
) {
|
||||||
FadedAnimationForSeekFeedback(visible = showFastSeekForward) {
|
FadedAnimationForSeekFeedback(visible = fastSeekModeForward) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
FastSeekVisualFeedback(
|
FastSeekVisualFeedback(
|
||||||
modifier = Modifier.align(Alignment.CenterStart),
|
modifier = Modifier.align(Alignment.CenterStart),
|
||||||
|
@ -186,7 +192,7 @@ fun GestureUI(
|
||||||
onRegularTap = defaultOnRegularTap,
|
onRegularTap = defaultOnRegularTap,
|
||||||
onMovement = handleDownwardMovement
|
onMovement = handleDownwardMovement
|
||||||
) {
|
) {
|
||||||
FadedAnimationForSeekFeedback(visible = showFastSeekBack) {
|
FadedAnimationForSeekFeedback(visible = fastSeekModeBackward) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
FastSeekVisualFeedback(
|
FastSeekVisualFeedback(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
@ -203,7 +209,7 @@ fun GestureUI(
|
||||||
onRegularTap = defaultOnRegularTap,
|
onRegularTap = defaultOnRegularTap,
|
||||||
onMovement = handleDownwardMovement
|
onMovement = handleDownwardMovement
|
||||||
) {
|
) {
|
||||||
FadedAnimationForSeekFeedback(visible = showFastSeekForward) {
|
FadedAnimationForSeekFeedback(visible = fastSeekModeForward) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
FastSeekVisualFeedback(
|
FastSeekVisualFeedback(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
@ -217,6 +223,7 @@ fun GestureUI(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FadedAnimationForSeekFeedback(visible: Boolean, content: @Composable () -> Unit) {
|
fun FadedAnimationForSeekFeedback(visible: Boolean, content: @Composable () -> Unit) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
|
@ -228,181 +235,7 @@ fun FadedAnimationForSeekFeedback(visible: Boolean, content: @Composable () -> U
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
private fun TouchSurface(
|
|
||||||
modifier: Modifier,
|
|
||||||
color: Color = Color.Transparent,
|
|
||||||
onDoubleTab: () -> Unit = {},
|
|
||||||
onRegularTap: () -> Unit = {},
|
|
||||||
onMovement: (TouchedPosition) -> Unit = {},
|
|
||||||
content: @Composable () -> Unit = {}
|
|
||||||
) {
|
|
||||||
var moveOccured by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastTouchedPosition by remember {
|
|
||||||
mutableStateOf(TouchedPosition(0f, 0f))
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastTouchTime by remember {
|
|
||||||
mutableStateOf(System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
|
|
||||||
val composableScope = rememberCoroutineScope()
|
|
||||||
var regularTabJob: Job? by remember {
|
|
||||||
mutableStateOf(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val defaultActionDown = { event: MotionEvent ->
|
|
||||||
lastTouchedPosition = TouchedPosition(event.x, event.y)
|
|
||||||
moveOccured = false
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val defaultActionUp = { onDoubleTap: () -> Unit, onRegularTap: () -> Unit ->
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
if (!moveOccured) {
|
|
||||||
val timeSinceLastTouch = currentTime - lastTouchTime
|
|
||||||
if (timeSinceLastTouch <= DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS) {
|
|
||||||
regularTabJob?.cancel()
|
|
||||||
onDoubleTap()
|
|
||||||
} else {
|
|
||||||
regularTabJob = composableScope.launch {
|
|
||||||
delay(DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS)
|
|
||||||
onRegularTap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
moveOccured = false
|
|
||||||
lastTouchTime = currentTime
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
val handleMove = { event: MotionEvent, lambda: (movement: TouchedPosition) -> Unit ->
|
|
||||||
val currentTouchedPosition = TouchedPosition(event.x, event.y)
|
|
||||||
val movement = currentTouchedPosition - lastTouchedPosition
|
|
||||||
lastTouchedPosition = currentTouchedPosition
|
|
||||||
moveOccured = true
|
|
||||||
lambda(movement)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = modifier.pointerInteropFilter {
|
|
||||||
when (it.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> defaultActionDown(it)
|
|
||||||
MotionEvent.ACTION_UP -> defaultActionUp(onDoubleTab, onRegularTap)
|
|
||||||
MotionEvent.ACTION_MOVE -> handleMove(it, onMovement)
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
content()
|
|
||||||
Surface(color = color, modifier = Modifier.fillMaxSize()) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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)
|
|
||||||
} else {
|
|
||||||
"Fast seeking forward by %d seconds."
|
|
||||||
//stringResource(id = R.string.fast_seeking_forward)
|
|
||||||
}, seconds
|
|
||||||
)
|
|
||||||
|
|
||||||
val infiniteTransition = rememberInfiniteTransition()
|
|
||||||
|
|
||||||
val animatedColor1 by infiniteTransition.animateColor(
|
|
||||||
initialValue = Color.White,
|
|
||||||
targetValue = Color.Transparent,
|
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = keyframes {
|
|
||||||
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
|
|
||||||
Color.White.copy(alpha = 1f) at 0 with LinearEasing
|
|
||||||
Color.White.copy(alpha = 0f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
|
|
||||||
},
|
|
||||||
repeatMode = RepeatMode.Restart
|
|
||||||
), label = "Arrow1 animation"
|
|
||||||
)
|
|
||||||
|
|
||||||
val animatedColor2 by infiniteTransition.animateColor(
|
|
||||||
initialValue = Color.White,
|
|
||||||
targetValue = Color.Transparent,
|
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = keyframes {
|
|
||||||
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
|
|
||||||
Color.White.copy(alpha = 1f / 3f) at 0 with LinearEasing
|
|
||||||
Color.White.copy(alpha = 0f) at SEEK_ANIMATION_DURATION_IN_MS / 3 with LinearEasing
|
|
||||||
Color.White.copy(alpha = 1f) at SEEK_ANIMATION_DURATION_IN_MS / 3 + 1 with LinearEasing
|
|
||||||
Color.White.copy(alpha = 2f / 3f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
|
|
||||||
},
|
|
||||||
repeatMode = RepeatMode.Restart
|
|
||||||
), label = "Arrow2 animation"
|
|
||||||
)
|
|
||||||
|
|
||||||
val animatedColor3 by infiniteTransition.animateColor(
|
|
||||||
initialValue = Color.White,
|
|
||||||
targetValue = Color.Transparent,
|
|
||||||
animationSpec = infiniteRepeatable(
|
|
||||||
animation = keyframes {
|
|
||||||
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
|
|
||||||
Color.White.copy(alpha = 2f / 3f) at 0 with LinearEasing
|
|
||||||
Color.White.copy(alpha = 0f) at 2 * SEEK_ANIMATION_DURATION_IN_MS / 3 with LinearEasing
|
|
||||||
Color.White.copy(alpha = 1f) at 2 * SEEK_ANIMATION_DURATION_IN_MS / 3 + 1 with LinearEasing
|
|
||||||
Color.White.copy(alpha = 2f / 3f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
|
|
||||||
},
|
|
||||||
repeatMode = RepeatMode.Restart
|
|
||||||
), label = "Arrow3 animation"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
//val secondsString = stringResource(id = R.string.seconds)
|
|
||||||
val secondsString = "Seconds"
|
|
||||||
|
|
||||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Row {
|
|
||||||
SeekerIcon(
|
|
||||||
backwards = backwards,
|
|
||||||
description = contentDescription,
|
|
||||||
color = if (backwards) animatedColor3 else animatedColor1
|
|
||||||
)
|
|
||||||
SeekerIcon(
|
|
||||||
backwards = backwards,
|
|
||||||
description = contentDescription,
|
|
||||||
color = animatedColor2
|
|
||||||
)
|
|
||||||
SeekerIcon(
|
|
||||||
backwards = backwards,
|
|
||||||
description = contentDescription,
|
|
||||||
color = if (backwards) animatedColor1 else animatedColor3
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(text = "$seconds $secondsString")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SeekerIcon(backwards: Boolean, description: String, color: Color) {
|
|
||||||
Icon(
|
|
||||||
modifier = if (backwards) {
|
|
||||||
Modifier.scale(-1f, 1f)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
},
|
|
||||||
tint = color,
|
|
||||||
painter = painterResource(id = R.drawable.ic_play_seek_triangle),
|
|
||||||
contentDescription = description
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -446,22 +279,3 @@ fun EmbeddedGestureUIPreview() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
|
||||||
@Composable
|
|
||||||
fun FastSeekVisualFeedbackPreviewBackwards() {
|
|
||||||
VideoPlayerTheme {
|
|
||||||
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
|
|
||||||
FastSeekVisualFeedback(seconds = 10, backwards = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
|
||||||
@Composable
|
|
||||||
fun FastSeekVisualFeedbackPreview() {
|
|
||||||
VideoPlayerTheme {
|
|
||||||
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
|
|
||||||
FastSeekVisualFeedback(seconds = 10, backwards = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
/* 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.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.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import net.newpipe.newplayer.R
|
||||||
|
import net.newpipe.newplayer.ui.theme.VideoPlayerTheme
|
||||||
|
import net.newpipe.newplayer.ui.videoplayer.SEEK_ANIMATION_DURATION_IN_MS
|
||||||
|
|
||||||
|
@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)
|
||||||
|
} else {
|
||||||
|
"Fast seeking forward by %d seconds."
|
||||||
|
//stringResource(id = R.string.fast_seeking_forward)
|
||||||
|
}, seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
val infiniteTransition = rememberInfiniteTransition()
|
||||||
|
|
||||||
|
val animatedColor1 by infiniteTransition.animateColor(
|
||||||
|
initialValue = Color.White,
|
||||||
|
targetValue = Color.Transparent,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = keyframes {
|
||||||
|
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
|
||||||
|
Color.White.copy(alpha = 1f) at 0 with LinearEasing
|
||||||
|
Color.White.copy(alpha = 0f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
|
||||||
|
},
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
), label = "Arrow1 animation"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedColor2 by infiniteTransition.animateColor(
|
||||||
|
initialValue = Color.White,
|
||||||
|
targetValue = Color.Transparent,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = keyframes {
|
||||||
|
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
|
||||||
|
Color.White.copy(alpha = 1f / 3f) at 0 with LinearEasing
|
||||||
|
Color.White.copy(alpha = 0f) at SEEK_ANIMATION_DURATION_IN_MS / 3 with LinearEasing
|
||||||
|
Color.White.copy(alpha = 1f) at SEEK_ANIMATION_DURATION_IN_MS / 3 + 1 with LinearEasing
|
||||||
|
Color.White.copy(alpha = 2f / 3f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
|
||||||
|
},
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
), label = "Arrow2 animation"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedColor3 by infiniteTransition.animateColor(
|
||||||
|
initialValue = Color.White,
|
||||||
|
targetValue = Color.Transparent,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = keyframes {
|
||||||
|
durationMillis = SEEK_ANIMATION_DURATION_IN_MS
|
||||||
|
Color.White.copy(alpha = 2f / 3f) at 0 with LinearEasing
|
||||||
|
Color.White.copy(alpha = 0f) at 2 * SEEK_ANIMATION_DURATION_IN_MS / 3 with LinearEasing
|
||||||
|
Color.White.copy(alpha = 1f) at 2 * SEEK_ANIMATION_DURATION_IN_MS / 3 + 1 with LinearEasing
|
||||||
|
Color.White.copy(alpha = 2f / 3f) at SEEK_ANIMATION_DURATION_IN_MS with LinearEasing
|
||||||
|
},
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
), label = "Arrow3 animation"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
//val secondsString = stringResource(id = R.string.seconds)
|
||||||
|
val secondsString = "Seconds"
|
||||||
|
|
||||||
|
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
Row {
|
||||||
|
SeekerIcon(
|
||||||
|
backwards = backwards,
|
||||||
|
description = contentDescription,
|
||||||
|
color = if (backwards) animatedColor3 else animatedColor1
|
||||||
|
)
|
||||||
|
SeekerIcon(
|
||||||
|
backwards = backwards,
|
||||||
|
description = contentDescription,
|
||||||
|
color = animatedColor2
|
||||||
|
)
|
||||||
|
SeekerIcon(
|
||||||
|
backwards = backwards,
|
||||||
|
description = contentDescription,
|
||||||
|
color = if (backwards) animatedColor1 else animatedColor3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(text = "$seconds $secondsString")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SeekerIcon(backwards: Boolean, description: String, color: Color) {
|
||||||
|
Icon(
|
||||||
|
modifier = if (backwards) {
|
||||||
|
Modifier.scale(-1f, 1f)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
tint = color,
|
||||||
|
painter = painterResource(id = R.drawable.ic_play_seek_triangle),
|
||||||
|
contentDescription = description
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
||||||
|
@Composable
|
||||||
|
fun FastSeekVisualFeedbackPreviewBackwards() {
|
||||||
|
VideoPlayerTheme {
|
||||||
|
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
|
||||||
|
FastSeekVisualFeedback(seconds = 10, backwards = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(device = "spec:width=1080px,height=600px,dpi=440,orientation=landscape")
|
||||||
|
@Composable
|
||||||
|
fun FastSeekVisualFeedbackPreview() {
|
||||||
|
VideoPlayerTheme {
|
||||||
|
Surface(modifier = Modifier.wrapContentSize(), color = Color.Black) {
|
||||||
|
FastSeekVisualFeedback(seconds = 10, backwards = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
/* 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.view.MotionEvent
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
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.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.newpipe.newplayer.ui.videoplayer.DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
fun TouchSurface(
|
||||||
|
modifier: Modifier,
|
||||||
|
color: Color = Color.Transparent,
|
||||||
|
onDoubleTab: () -> Unit = {},
|
||||||
|
onRegularTap: () -> Unit = {},
|
||||||
|
onMovement: (TouchedPosition) -> Unit = {},
|
||||||
|
content: @Composable () -> Unit = {}
|
||||||
|
) {
|
||||||
|
var moveOccured by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastTouchedPosition by remember {
|
||||||
|
mutableStateOf(TouchedPosition(0f, 0f))
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastTouchTime by remember {
|
||||||
|
mutableStateOf(System.currentTimeMillis())
|
||||||
|
}
|
||||||
|
|
||||||
|
val composableScope = rememberCoroutineScope()
|
||||||
|
var regularTabJob: Job? by remember {
|
||||||
|
mutableStateOf(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultActionDown = { event: MotionEvent ->
|
||||||
|
lastTouchedPosition = TouchedPosition(event.x, event.y)
|
||||||
|
moveOccured = false
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultActionUp = { onDoubleTap: () -> Unit, onRegularTap: () -> Unit ->
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
if (!moveOccured) {
|
||||||
|
val timeSinceLastTouch = currentTime - lastTouchTime
|
||||||
|
if (timeSinceLastTouch <= DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS) {
|
||||||
|
regularTabJob?.cancel()
|
||||||
|
onDoubleTap()
|
||||||
|
} else {
|
||||||
|
regularTabJob = composableScope.launch {
|
||||||
|
delay(DELAY_UNTIL_SHOWING_UI_AFTER_TOUCH_IN_MS)
|
||||||
|
onRegularTap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moveOccured = false
|
||||||
|
lastTouchTime = currentTime
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
val handleMove = { event: MotionEvent, lambda: (movement: TouchedPosition) -> Unit ->
|
||||||
|
val currentTouchedPosition = TouchedPosition(event.x, event.y)
|
||||||
|
val movement = currentTouchedPosition - lastTouchedPosition
|
||||||
|
lastTouchedPosition = currentTouchedPosition
|
||||||
|
moveOccured = true
|
||||||
|
lambda(movement)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = modifier.pointerInteropFilter {
|
||||||
|
when (it.action) {
|
||||||
|
MotionEvent.ACTION_DOWN -> defaultActionDown(it)
|
||||||
|
MotionEvent.ACTION_UP -> defaultActionUp(onDoubleTab, onRegularTap)
|
||||||
|
MotionEvent.ACTION_MOVE -> handleMove(it, onMovement)
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
content()
|
||||||
|
Surface(color = color, modifier = Modifier.fillMaxSize()) {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
data class TouchedPosition(val x: Float, val y: Float) {
|
||||||
|
operator fun minus(other: TouchedPosition) = TouchedPosition(this.x - other.x, this.y - other.y)
|
||||||
|
}
|
Loading…
Reference in New Issue