Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
105 changes: 0 additions & 105 deletions ui/src/main/java/com/theoplayer/android/ui/Modifiers.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<PressInteraction.Press?>(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<Boolean>,
showControlsTemporarily: () -> Unit,
Expand Down
14 changes: 8 additions & 6 deletions ui/src/main/java/com/theoplayer/android/ui/Player.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -423,7 +425,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player

private val volumeChangeListener = EventListener<VolumeChangeEvent> { updateVolumeAndMuted() }

private var _playbackRate by mutableStateOf(1.0)
private var _playbackRate by mutableDoubleStateOf(1.0)
override var playbackRate: Double
get() = _playbackRate
set(value) {
Expand Down
62 changes: 24 additions & 38 deletions ui/src/main/java/com/theoplayer/android/ui/UIController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -101,7 +95,6 @@ fun UIController(
UIController(
modifier = modifier,
player = player,
interactionSource = interactionSource,
color = color,
centerOverlay = centerOverlay,
errorOverlay = errorOverlay,
Expand All @@ -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,
Expand All @@ -136,15 +126,14 @@ 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,
topChrome: (@Composable UIControllerScope.() -> Unit)? = null,
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) {
Expand All @@ -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,
Expand All @@ -175,7 +163,7 @@ fun UIController(
} else if (forceControlsHidden) {
false
} else {
isRecentlyTapped || isPressed || player.paused
isRecentlyTapped || player.paused
}
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
}
Expand All @@ -348,7 +334,7 @@ private fun PlayerContainer(
var composeView by remember { mutableStateOf<ComposeView?>(null) }

AndroidView(
modifier = containerModifier,
modifier = modifier,
factory = { context ->
uiContainer =
theoplayerView.findViewById(com.theoplayer.android.R.id.theo_ui_container)
Expand Down
Loading