diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e9d8fa..139794e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ > - 🏠 Internal > - 💅 Polish +## v1.11.1 (2025-08-01) + +* 🐛 Fixed clicking on overlays from OptiView Ads not working. ([#68](https://github.com/THEOplayer/android-ui/pull/68)) + ## v1.11.0 (2025-04-29) * 💥 Bumped `compileSdk` to API 35 (Android 15). diff --git a/gradle.properties b/gradle.properties index 6d472d8..2c93375 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,4 +27,4 @@ org.gradle.configuration-cache=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true # The version of the THEOplayer Open Video UI for Android. -version=1.11.0 +version=1.11.1 diff --git a/ui/src/main/java/com/theoplayer/android/ui/Modifiers.kt b/ui/src/main/java/com/theoplayer/android/ui/Modifiers.kt index 0ca2542..a336f00 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Modifiers.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Modifiers.kt @@ -1,28 +1,11 @@ package com.theoplayer.android.ui import androidx.annotation.FloatRange -import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.waitForUpOrCancellation -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -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.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass -import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope -import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed -import androidx.compose.ui.input.pointer.isOutOfBounds import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope @@ -42,94 +25,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlin.math.roundToInt -internal fun Modifier.pressable( - interactionSource: MutableInteractionSource, - enabled: Boolean = true, - requireUnconsumed: Boolean = true -): Modifier = composed( - inspectorInfo = debugInspectorInfo { - name = "pressable" - properties["interactionSource"] = interactionSource - properties["enabled"] = enabled - properties["requireUnconsumed"] = requireUnconsumed - } -) { - val scope = rememberCoroutineScope() - var pressedInteraction by remember { mutableStateOf(null) } - - suspend fun emitPress(pressPosition: Offset) { - if (pressedInteraction == null) { - val interaction = PressInteraction.Press(pressPosition) - interactionSource.emit(interaction) - pressedInteraction = interaction - } - } - - suspend fun emitRelease() { - pressedInteraction?.let { oldValue -> - val interaction = PressInteraction.Release(oldValue) - interactionSource.emit(interaction) - pressedInteraction = null - } - } - - fun tryEmitCancel() { - pressedInteraction?.let { oldValue -> - val interaction = PressInteraction.Cancel(oldValue) - interactionSource.tryEmit(interaction) - pressedInteraction = null - } - } - - DisposableEffect(interactionSource) { - onDispose { tryEmitCancel() } - } - LaunchedEffect(enabled) { - if (!enabled) { - emitRelease() - } - } - - if (enabled) { - Modifier - .pointerInput(interactionSource) { - val currentContext = currentCoroutineContext() - awaitPointerEventScope { - while (currentContext.isActive) { - val down = awaitFirstDown(requireUnconsumed = requireUnconsumed) - scope.launch { emitPress(down.position) } - val up = - if (requireUnconsumed) waitForUpOrCancellation() else waitForUpOrCancellationIgnoreConsumed() - if (up == null) { - tryEmitCancel() - } else { - scope.launch { emitRelease() } - } - } - } - } - } else { - Modifier - } -} - -/** - * Like [AwaitPointerEventScope.waitForUpOrCancellation], - * but skips the [PointerInputChange.isConsumed] checks. - */ -private suspend fun AwaitPointerEventScope.waitForUpOrCancellationIgnoreConsumed(): PointerInputChange? { - while (true) { - val event = awaitPointerEvent(PointerEventPass.Main) - if (event.changes.all { it.changedToUpIgnoreConsumed() }) { - // All pointers are up - return event.changes[0] - } - if (event.changes.any { it.isOutOfBounds(size, extendedTouchPadding) }) { - return null // Canceled - } - } -} - internal fun Modifier.toggleControlsOnTap( controlsVisible: State, showControlsTemporarily: () -> Unit, diff --git a/ui/src/main/java/com/theoplayer/android/ui/Player.kt b/ui/src/main/java/com/theoplayer/android/ui/Player.kt index 93b098c..c744785 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -296,9 +298,9 @@ enum class StreamType { internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player { override val player = theoplayerView?.player override val ads = theoplayerView?.player?.ads - override var currentTime by mutableStateOf(0.0) + override var currentTime by mutableDoubleStateOf(0.0) private set - override var duration by mutableStateOf(Double.NaN) + override var duration by mutableDoubleStateOf(Double.NaN) private set override var seekable by mutableStateOf(TimeRanges.empty()) private set @@ -312,9 +314,9 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player private set override var readyState by mutableStateOf(ReadyState.HAVE_NOTHING) private set - override var videoWidth by mutableStateOf(0) + override var videoWidth by mutableIntStateOf(0) private set - override var videoHeight by mutableStateOf(0) + override var videoHeight by mutableIntStateOf(0) private set override var firstPlay by mutableStateOf(false) private set @@ -401,7 +403,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player player?.source = value } - private var _volume by mutableStateOf(1.0) + private var _volume by mutableDoubleStateOf(1.0) private var _muted by mutableStateOf(false) override var volume: Double get() = _volume @@ -423,7 +425,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player private val volumeChangeListener = EventListener { updateVolumeAndMuted() } - private var _playbackRate by mutableStateOf(1.0) + private var _playbackRate by mutableDoubleStateOf(1.0) override var playbackRate: Double get() = _playbackRate set(value) { diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index 7385215..9b207b8 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -17,9 +17,6 @@ import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.Interaction -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -32,6 +29,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -68,9 +66,6 @@ import kotlin.time.DurationUnit * @param modifier the [Modifier] to be applied to this container * @param config the player configuration to be used when constructing the [THEOplayerView] * @param source the source description to load into the player - * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s - * for this container. You can create and pass in your own `remember`ed instance to observe - * [Interaction]s and customize the behavior of this container. * @param color the background color for the overlay while showing the UI controls * @param centerOverlay content to show in the center of the player, typically a [LoadingSpinner]. * @param errorOverlay content to show when the player encountered a fatal error, @@ -85,7 +80,6 @@ fun UIController( modifier: Modifier = Modifier, config: THEOplayerConfig, source: SourceDescription? = null, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, color: Color = Color.Black, centerOverlay: (@Composable UIControllerScope.() -> Unit)? = null, errorOverlay: (@Composable UIControllerScope.() -> Unit)? = null, @@ -101,7 +95,6 @@ fun UIController( UIController( modifier = modifier, player = player, - interactionSource = interactionSource, color = color, centerOverlay = centerOverlay, errorOverlay = errorOverlay, @@ -120,9 +113,6 @@ fun UIController( * * @param modifier the [Modifier] to be applied to this container * @param player the player. This should always be created using [rememberPlayer]. - * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s - * for this container. You can create and pass in your own `remember`ed instance to observe - * [Interaction]s and customize the behavior of this container. * @param color the background color for the overlay while showing the UI controls * @param centerOverlay content to show in the center of the player, typically a [LoadingSpinner]. * @param errorOverlay content to show when the player encountered a fatal error, @@ -136,7 +126,6 @@ fun UIController( fun UIController( modifier: Modifier = Modifier, player: Player = rememberPlayer(), - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, color: Color = Color.Black, centerOverlay: (@Composable UIControllerScope.() -> Unit)? = null, errorOverlay: (@Composable UIControllerScope.() -> Unit)? = null, @@ -144,7 +133,7 @@ fun UIController( centerChrome: (@Composable UIControllerScope.() -> Unit)? = null, bottomChrome: (@Composable UIControllerScope.() -> Unit)? = null ) { - var tapCount by remember { mutableStateOf(0) } + var tapCount by remember { mutableIntStateOf(0) } var isRecentlyTapped by remember { mutableStateOf(false) } LaunchedEffect(tapCount) { if (tapCount > 0) { @@ -153,7 +142,6 @@ fun UIController( isRecentlyTapped = false } } - val isPressed by interactionSource.collectIsPressedAsState() var forceControlsHidden by remember { mutableStateOf(false) } // Wait a little bit before showing the controls and enabling animations, @@ -175,7 +163,7 @@ fun UIController( } else if (forceControlsHidden) { false } else { - isRecentlyTapped || isPressed || player.paused + isRecentlyTapped || player.paused } } } @@ -216,27 +204,29 @@ fun UIController( ) ) - PlayerContainer(modifier = modifier, player = player) { + PlayerContainer( + player = player, + modifier = Modifier + .background(Color.Black) + .then(modifier) + .playerAspectRatio(player) + .toggleControlsOnTap( + controlsVisible = controlsVisible, + showControlsTemporarily = { + forceControlsHidden = false + tapCount++ + }, + hideControls = { + forceControlsHidden = true + tapCount++ + } + ) + ) { CompositionLocalProvider(LocalPlayer provides player) { - if (player.playingAd) { - // Remove player UI entirely while playing an ad, to make clickthrough work - return@CompositionLocalProvider - } AnimatedContent( label = "ContentAnimation", modifier = Modifier - .background(background) - .pressable(interactionSource = interactionSource, requireUnconsumed = false) - .toggleControlsOnTap( - controlsVisible = controlsVisible, - showControlsTemporarily = { - forceControlsHidden = false - tapCount++ - }, - hideControls = { - forceControlsHidden = true - tapCount++ - }), + .background(background), targetState = uiState, transitionSpec = { if (targetState is UIState.Error) { @@ -332,13 +322,9 @@ private fun PlayerContainer( ui: @Composable () -> Unit ) { val theoplayerView = player.theoplayerView - val containerModifier = Modifier - .background(Color.Black) - .then(modifier) - .playerAspectRatio(player) if (theoplayerView == null) { Box( - modifier = containerModifier + modifier = modifier ) { ui() } @@ -348,7 +334,7 @@ private fun PlayerContainer( var composeView by remember { mutableStateOf(null) } AndroidView( - modifier = containerModifier, + modifier = modifier, factory = { context -> uiContainer = theoplayerView.findViewById(com.theoplayer.android.R.id.theo_ui_container)