Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Forward/Rewind #40

Merged
merged 10 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

[*.{kt,kts}]
#Custom configuration
ktlint_disabled_rules = no-wildcard-imports
insert_final_newline = true
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.profusion.androidenhancedvideoplayer.test

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.doubleClick
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import com.profusion.androidenhancedvideoplayer.components.EnhancedVideoPlayer
import org.junit.Rule
import org.junit.Test
Expand All @@ -29,4 +32,38 @@ class EnhancedVideoPlayerTest {
composeTestRule.onNodeWithTag("PlayerControlsParent", useUnmergedTree = true)
.assertIsDisplayed()
}

@Test
fun enhancedVideoPlayer_WhenDoubleClickHappenOnTheFirstHalfOfScreenVideoShouldShowRewindIcon() {
ricardodalarme marked this conversation as resolved.
Show resolved Hide resolved
composeTestRule.setContent {
EnhancedVideoPlayer(
resourceId = R.raw.login_screen_background
)
}

composeTestRule.onAllNodesWithTag("SeekClickableArea", useUnmergedTree = true)[0]
.performTouchInput {
doubleClick()
}

composeTestRule.onNodeWithTag("RewindIcon", useUnmergedTree = true)
.assertIsDisplayed()
}
mari1912 marked this conversation as resolved.
Show resolved Hide resolved

@Test
fun enhancedVideoPlayer_WhenDoubleClickHappenOnTheLastHalfOfScreenVideoShouldShowRewindIcon() {
composeTestRule.setContent {
EnhancedVideoPlayer(
resourceId = R.raw.login_screen_background
)
}

composeTestRule.onAllNodesWithTag("SeekClickableArea", useUnmergedTree = true)[1]
.performTouchInput {
doubleClick()
}

composeTestRule.onNodeWithTag("ForwardIcon", useUnmergedTree = true)
.assertIsDisplayed()
}
mari1912 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package com.profusion.androidenhancedvideoplayer.components

import android.content.res.Configuration
import android.net.Uri
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
Expand All @@ -14,10 +18,13 @@ 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.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
Expand All @@ -27,6 +34,7 @@ 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.components.playerOverlay.SeekHandler
import com.profusion.androidenhancedvideoplayer.components.playerOverlay.SettingsControlsCustomization
import com.profusion.androidenhancedvideoplayer.utils.TimeoutEffect
import com.profusion.androidenhancedvideoplayer.utils.fillMaxSizeOnLandscape
Expand All @@ -38,6 +46,8 @@ import com.profusion.androidenhancedvideoplayer.utils.setStatusBarVisibility
private const val MAIN_PACKAGE_PATH_PREFIX = "android.resource://"
private const val CURRENT_TIME_TICK_IN_MS = 50L

private const val DEFAULT_SEEK_TIME_MS = 10 * 1000L // 10 seconds

@androidx.annotation.OptIn(UnstableApi::class)
@Composable
fun EnhancedVideoPlayer(
Expand All @@ -49,7 +59,8 @@ fun EnhancedVideoPlayer(
soundOff: Boolean = true,
currentTimeTickInMs: Long = CURRENT_TIME_TICK_IN_MS,
controlsCustomization: ControlsCustomization = ControlsCustomization(),
settingsControlsCustomization: SettingsControlsCustomization = SettingsControlsCustomization()
settingsControlsCustomization: SettingsControlsCustomization = SettingsControlsCustomization(),
transformSeekIncrementRatio: (tapCount: Int) -> Long = { it -> it * DEFAULT_SEEK_TIME_MS }
) {
val context = LocalContext.current
val mainPackagePath = "$MAIN_PACKAGE_PATH_PREFIX${context.packageName}/"
Expand All @@ -65,7 +76,8 @@ fun EnhancedVideoPlayer(
soundOff = soundOff,
currentTimeTickInMs = currentTimeTickInMs,
controlsCustomization = controlsCustomization,
settingsControlsCustomization = settingsControlsCustomization
settingsControlsCustomization = settingsControlsCustomization,
transformSeekIncrementRatio = { transformSeekIncrementRatio(it) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
transformSeekIncrementRatio = { transformSeekIncrementRatio(it) }
transformSeekIncrementRatio = transformSeekIncrementRatio

)
}

Expand All @@ -80,6 +92,7 @@ fun EnhancedVideoPlayer(
soundOff: Boolean = true,
currentTimeTickInMs: Long = CURRENT_TIME_TICK_IN_MS,
controlsCustomization: ControlsCustomization = ControlsCustomization(),
transformSeekIncrementRatio: (tapCount: Int) -> Long = { it -> it * DEFAULT_SEEK_TIME_MS },
settingsControlsCustomization: SettingsControlsCustomization = SettingsControlsCustomization()
) {
val context = LocalContext.current
Expand All @@ -96,7 +109,6 @@ fun EnhancedVideoPlayer(
prepare()
}
}

var isPlaying by remember { mutableStateOf(exoPlayer.isPlaying) }
var hasEnded by remember { mutableStateOf(exoPlayer.playbackState == ExoPlayer.STATE_ENDED) }
var isControlsVisible by remember { mutableStateOf(false) }
Expand All @@ -114,6 +126,10 @@ fun EnhancedVideoPlayer(
context.setNavigationBarVisibility(shouldShowSystemUi)
}

fun setControlsVisibility(visible: Boolean) {
isControlsVisible = visible
}

DisposableEffect(context) {
val listener = object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
Expand Down Expand Up @@ -142,11 +158,6 @@ fun EnhancedVideoPlayer(

Box(
modifier = Modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { isControlsVisible = !isControlsVisible }
)
.background(Color.Black)
.fillMaxSizeOnLandscape(orientation)
.testTag("VideoPlayerParent"),
Expand All @@ -167,6 +178,18 @@ fun EnhancedVideoPlayer(
}
}
)
Box(modifier = Modifier.matchParentSize()) {
SeekHandler(
disableSeekForward = hasEnded,
isControlsVisible = isControlsVisible,
exoPlayer = exoPlayer,
controlsCustomization = controlsCustomization,
toggleControlsVisibility = { isControlsVisible = !isControlsVisible },
setControlsVisibility = ::setControlsVisibility,
transformSeekIncrementRatio = transformSeekIncrementRatio
)
}

PlayerControls(
title = title,
isVisible = isControlsVisible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ class ControlsCustomization(
val nextIconContent: @Composable () -> Unit = { NextIcon() },
val fullScreenIconContent: @Composable () -> Unit = { FullScreenIcon() },
val exitFullScreenIconContent: @Composable () -> Unit = { ExitFullScreenIcon() },
val settingsIconContent: @Composable () -> Unit = { SettingsIcon() }
val settingsIconContent: @Composable () -> Unit = { SettingsIcon() },
val forwardIconContent: @Composable (modifier: Modifier) -> Unit = { ForwardIcon(it) },
val rewindIconContent: @Composable (modifier: Modifier) -> Unit = { RewindIcon(it) }
)

@Composable
fun PlayerControls(
modifier: Modifier = Modifier,
title: String? = null,
isVisible: Boolean,
isPlaying: Boolean,
Expand All @@ -33,8 +36,7 @@ fun PlayerControls(
onSpeedSelected: (Float) -> Unit,
onSeekBarValueChange: (Long) -> Unit,
customization: ControlsCustomization,
settingsControlsCustomization: SettingsControlsCustomization,
modifier: Modifier = Modifier
settingsControlsCustomization: SettingsControlsCustomization
) {
PlayerControlsScaffold(
modifier = modifier.testTag("PlayerControlsParent"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import com.profusion.androidenhancedvideoplayer.styling.Colors

@Composable
fun PlayerControlsScaffold(
Expand All @@ -25,7 +25,7 @@ fun PlayerControlsScaffold(
enter = fadeIn(),
exit = fadeOut(),
modifier = modifier
.background(Color.Black.copy(alpha = 0.6f))
.background(Colors.controlsShadow)
) {
Column(
modifier = Modifier.fillMaxSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,23 @@ fun CheckIcon(modifier: Modifier = Modifier) {
modifier = modifier
)
}

@Composable
fun ForwardIcon(modifier: Modifier = Modifier) {
Icon(
painter = painterResource(id = R.drawable.ic_forward),
tint = Color.White,
contentDescription = stringResource(R.string.controls_forward_description),
modifier = modifier.testTag("ForwardIcon")
mari1912 marked this conversation as resolved.
Show resolved Hide resolved
)
}

@Composable
fun RewindIcon(modifier: Modifier = Modifier) {
Icon(
painter = painterResource(id = R.drawable.ic_rewind),
tint = Color.White,
contentDescription = stringResource(R.string.controls_rewind_description),
modifier = modifier.testTag("RewindIcon")
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.profusion.androidenhancedvideoplayer.components.playerOverlay

import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.input.pointer.pointerInput
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import com.profusion.androidenhancedvideoplayer.R
import com.profusion.androidenhancedvideoplayer.styling.Dimensions

private const val TEXT_MAX_LINES = 2

@Composable
fun SeekClickableArea(
modifier: Modifier = Modifier,
scaleAnimation: Float,
tapCount: Int,
disableSeekClick: Boolean = false,
onSeekSingleTap: () -> Unit,
onSeekDoubleTap: () -> Unit,
checkIfCanToggleIsControlsVisible: () -> Unit,
getSeekTime: () -> Int,
seekIcon: @Composable (modifier: Modifier) -> Unit
) {
val isTapCountGreaterThanZero = tapCount > 0
Box(
modifier = Modifier
.fillMaxHeight()
.indication(
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
.pointerInput(tapCount, disableSeekClick) {
detectTapGestures(
onTap = if (isTapCountGreaterThanZero) {
if (disableSeekClick) {
null
} else { { onSeekSingleTap() } }
} else {
{ checkIfCanToggleIsControlsVisible() }
},
onDoubleTap = if (isTapCountGreaterThanZero) { null } else {
if (disableSeekClick) {
null
} else {
{ onSeekDoubleTap() }
}
}
)
Comment on lines +48 to +62
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: now you have more than one state, so it would be better to use when instead of if 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now i need the if because this additional state needs to be nested 🤣

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that the check need to happen only when the isTapCountGreaterThanZero is true

}
.testTag("SeekClickableArea")
.then(modifier),
contentAlignment = Alignment.Center
) {
if (isTapCountGreaterThanZero) {
val timeLabel = getSeekTime()
Column(horizontalAlignment = Alignment.CenterHorizontally) {
seekIcon(modifier = Modifier.scale(scaleAnimation))
Spacer(modifier = Modifier.height(Dimensions.large))
Text(
ricardodalarme marked this conversation as resolved.
Show resolved Hide resolved
text = "$timeLabel ${stringResource(id = R.string.controls_time_unit)}",
maxLines = TEXT_MAX_LINES,
color = Color.White
)
}
}
}
}
Loading