diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt index f105d613..d5a05e95 100644 --- a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt +++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/components/EnhancedVideoPlayer.kt @@ -2,30 +2,63 @@ package com.profusion.androidenhancedvideoplayer.components import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.net.Uri +import androidx.compose.animation.* +import androidx.compose.animation.core.* +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.* import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect 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.* +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.* +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import com.profusion.androidenhancedvideoplayer.components.playerOverlay.ControlsCustomization -import com.profusion.androidenhancedvideoplayer.components.playerOverlay.PlayerControls -import com.profusion.androidenhancedvideoplayer.utils.setLandscape -import com.profusion.androidenhancedvideoplayer.utils.setPortrait +import com.profusion.androidenhancedvideoplayer.utils.executeAfterTimeout +import kotlinx.coroutines.Job + +data class SeekState( + val isSeekEnabled: Boolean, + val tapCount: Int +) private const val MAIN_PACKAGE_PATH_PREFIX = "android.resource://" @@ -41,6 +74,7 @@ fun EnhancedVideoPlayer( ) { val context = LocalContext.current val configuration = LocalConfiguration.current + val scope = rememberCoroutineScope() val mainPackagePath = "$MAIN_PACKAGE_PATH_PREFIX${context.packageName}/" val exoPlayer = remember { @@ -62,6 +96,39 @@ fun EnhancedVideoPlayer( var hasEnded by remember { mutableStateOf(exoPlayer.playbackState == ExoPlayer.STATE_ENDED) } var isControlsVisible by remember { mutableStateOf(false) } val isFullScreen = configuration.orientation == ORIENTATION_LANDSCAPE + var backwardState by remember { + mutableStateOf( + SeekState( + isSeekEnabled = false, + tapCount = 0 + ) + ) + } + + var backwardJob: Job? = null + + fun setBackwardState(isSeekEnabled: Boolean, shouldResetTapCount: Boolean = false) { + backwardState = backwardState.copy( + isSeekEnabled = isSeekEnabled, + tapCount = if (shouldResetTapCount) 0 else backwardState.tapCount + 1 + ) + } + + fun onBackwardSingleTap() { + Log.d("BUTTON", "backward tap") + Log.d("BUTTON", "${backwardJob}") + setBackwardState(isSeekEnabled = true) + backwardJob = executeAfterTimeout(scope, backwardJob) { setBackwardState(false) } + } + + fun onBackwardDoubletap() { + setBackwardState(isSeekEnabled = true) + Log.d("BUTTON", "backward double tap") + backwardJob = executeAfterTimeout(scope, backwardJob) { + Log.d("BUTTON", "disable state after timeout called") + setBackwardState(false, shouldResetTapCount = true) + } + } DisposableEffect(context) { val listener = object : Player.Listener { @@ -103,26 +170,138 @@ fun EnhancedVideoPlayer( } } ) - PlayerControls( - isVisible = isControlsVisible, - isPlaying = isPlaying, - isFullScreen = isFullScreen, - hasEnded = hasEnded, - onPreviousClick = exoPlayer::seekToPrevious, - onNextClick = exoPlayer::seekToNext, - onPauseToggle = when { - hasEnded -> exoPlayer::seekToDefaultPosition - isPlaying -> exoPlayer::pause - else -> exoPlayer::play - }, - onFullScreenToggle = { - when (isFullScreen) { - true -> context.setPortrait() - false -> context.setLandscape() + Row( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .pointerInput(backwardState) { + Log.d("BUTTON", "executed pointerInput After state change") + detectTapGestures( + onTap = when (backwardState.isSeekEnabled) { + true -> { _ -> onBackwardSingleTap() } + false -> null }, + onDoubleTap = when (backwardState.isSeekEnabled) { + true -> null + false -> { _ -> onBackwardDoubletap() } + } + ) + } + .background(Color.Red.copy(alpha = 0.4f)) + ) + Box( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .clickable { Log.d("BUTTON", "rightside clicked") } + .background(Color.Green.copy(alpha = 0.4f)) + ) + } + Button( + onClick = { Log.d("BUTTON", "botão clicado") }, + modifier = Modifier + .align(Alignment.Center) + .padding(16.dp) + ) { + Text("Clique aqui") + } + +// PlayerControls( +// isVisible = isControlsVisible, +// isPlaying = isPlaying, +// isFullScreen = isFullScreen, +// hasEnded = hasEnded, +// onPreviousClick = exoPlayer::seekToPrevious, +// onNextClick = exoPlayer::seekToNext, +// onPauseToggle = when { +// hasEnded -> exoPlayer::seekToDefaultPosition +// isPlaying -> exoPlayer::pause +// else -> exoPlayer::play +// }, +// onFullScreenToggle = { +// when (isFullScreen) { +// true -> context.setPortrait() +// false -> context.setLandscape() +// } +// }, +// customization = controlsCustomization, +// modifier = Modifier.matchParentSize().testTag("PlayerControlsParent") +// ) + } +} + +@ExperimentalAnimationApi +@Composable +fun AnimatedIcon() { + var isVisible by remember { mutableStateOf(false) } + val transition = updateTransition(isVisible, label = "") + + val scale by transition.animateFloat( + transitionSpec = { + keyframes { + durationMillis = 300 + 0f at 0 + 1f at 150 + 0f at 300 + } + } + ) { state -> + if (state) 1f else 0f + } + + val alpha by transition.animateFloat( + transitionSpec = { + keyframes { + durationMillis = 300 + 0f at 0 + 1f at 150 + 0f at 300 + } + } + ) { state -> + if (state) 1f else 0f + } + + Column( + modifier = Modifier + .fillMaxWidth() + .height(90.dp) + .wrapContentHeight() + .clickable { + if (transition.currentState == false) { + isVisible = true + } else { + isVisible = false } - }, - customization = controlsCustomization, - modifier = Modifier.matchParentSize().testTag("PlayerControlsParent") - ) + } + .padding(16.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedVisibility( + visible = isVisible, + enter = fadeIn() + expandIn(), + exit = fadeOut() + shrinkOut() + ) { + Box( + modifier = Modifier + .size(48.dp) + .graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + } + ) { + Icon( + imageVector = Icons.Default.Favorite, + contentDescription = null, + tint = Color.Red + ) + } + } } } diff --git a/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/ExecuteAfterTimeout.kt b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/ExecuteAfterTimeout.kt new file mode 100644 index 00000000..6aff12b1 --- /dev/null +++ b/androidenhancedvideoplayer/src/main/java/com/profusion/androidenhancedvideoplayer/utils/ExecuteAfterTimeout.kt @@ -0,0 +1,26 @@ +package com.profusion.androidenhancedvideoplayer.utils + +import android.util.Log +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +fun executeAfterTimeout( + scope: CoroutineScope, + job: Job?, + timeInMillis: Long = 2000L, + onJobComplete: () -> Unit +): Job { + job?.cancel() + return scope.launch() { + try { + delay(timeInMillis) + onJobComplete() + } catch (e: CancellationException) { + Log.d("BUTTON", "timeOut reseted") + } + } +} \ No newline at end of file