Skip to content

Commit

Permalink
Merge pull request #16 from THEOplayer/player-aspect-ratio
Browse files Browse the repository at this point in the history
Make player UI use video aspect ratio by default
  • Loading branch information
MattiasBuelens authored Jul 13, 2023
2 parents 4964efa + c165f0e commit a9c4724
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 14 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
> - 🏠 Internal
> - 💅 Polish
## Unreleased

* 💅 `UIController` now sizes itself to match the video's aspect ratio, except if this were to
conflict with a different size constraint (such as `Modifier.fillMaxSize()`).

## v1.3.2 (2023-07-13)

* 🏠 Publish to THEOplayer's own Maven repository.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.theoplayer.android.ui.demo
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
Expand Down Expand Up @@ -85,13 +84,7 @@ fun MainContent() {
}, content = { padding ->
val playerModifier = Modifier
.padding(padding)
.aspectRatio(
if (player.videoWidth != 0 && player.videoHeight != 0) {
player.videoWidth.toFloat() / player.videoHeight.toFloat()
} else {
16f / 9f
}
)
.fillMaxSize(1f)
when (theme) {
PlayerTheme.Default -> {
DefaultUI(
Expand Down
189 changes: 186 additions & 3 deletions ui/src/main/java/com/theoplayer/android/ui/Modifiers.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
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.*
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.*
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
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.InspectorValueInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.isSatisfiedBy
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.math.roundToInt

internal fun Modifier.pressable(
interactionSource: MutableInteractionSource,
Expand Down Expand Up @@ -150,4 +176,161 @@ private suspend fun PointerInputScope.detectAnyPointerEvent(
onPointer()
}
}
}
}

internal fun Modifier.constrainedAspectRatio(
@FloatRange(from = 0.0, fromInclusive = false)
ratio: Float,
matchHeightConstraintsFirst: Boolean = false
): Modifier = this.then(
ConstrainedAspectRatioModifier(
ratio,
matchHeightConstraintsFirst,
debugInspectorInfo {
name = "constrainedAspectRatio"
properties["ratio"] = ratio
properties["matchHeightConstraintsFirst"] = matchHeightConstraintsFirst
}
)
)

private class ConstrainedAspectRatioModifier(
val aspectRatio: Float,
val matchHeightConstraintsFirst: Boolean,
inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
init {
require(aspectRatio > 0) { "aspectRatio $aspectRatio must be > 0" }
}

override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val size = constraints.findSize()
val wrappedConstraints = if (size != IntSize.Zero) {
Constraints.fixed(size.width, size.height)
} else {
constraints
}
val placeable = measurable.measure(wrappedConstraints)
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}

override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
) = if (height != Constraints.Infinity) {
(height * aspectRatio).roundToInt()
} else {
measurable.minIntrinsicWidth(height)
}

override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
) = if (height != Constraints.Infinity) {
(height * aspectRatio).roundToInt()
} else {
measurable.maxIntrinsicWidth(height)
}

override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = if (width != Constraints.Infinity) {
(width / aspectRatio).roundToInt()
} else {
measurable.minIntrinsicHeight(width)
}

override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
) = if (width != Constraints.Infinity) {
(width / aspectRatio).roundToInt()
} else {
measurable.maxIntrinsicHeight(width)
}

private fun Constraints.findSize(): IntSize {
if (!matchHeightConstraintsFirst) {
tryMaxWidth().also { if (it != IntSize.Zero) return it }
tryMaxHeight().also { if (it != IntSize.Zero) return it }
tryMinWidth().also { if (it != IntSize.Zero) return it }
tryMinHeight().also { if (it != IntSize.Zero) return it }
} else {
tryMaxHeight().also { if (it != IntSize.Zero) return it }
tryMaxWidth().also { if (it != IntSize.Zero) return it }
tryMinHeight().also { if (it != IntSize.Zero) return it }
tryMinWidth().also { if (it != IntSize.Zero) return it }
}
return IntSize.Zero
}

private fun Constraints.tryMaxWidth(): IntSize {
val maxWidth = this.maxWidth
if (maxWidth != Constraints.Infinity) {
val height = (maxWidth / aspectRatio).roundToInt()
if (height > 0) {
val size = IntSize(maxWidth, height)
if (isSatisfiedBy(size)) {
return size
}
}
}
return IntSize.Zero
}

private fun Constraints.tryMaxHeight(): IntSize {
val maxHeight = this.maxHeight
if (maxHeight != Constraints.Infinity) {
val width = (maxHeight * aspectRatio).roundToInt()
if (width > 0) {
val size = IntSize(width, maxHeight)
if (isSatisfiedBy(size)) {
return size
}
}
}
return IntSize.Zero
}

private fun Constraints.tryMinWidth(): IntSize {
val minWidth = this.minWidth
val height = (minWidth / aspectRatio).roundToInt()
if (height > 0) {
val size = IntSize(minWidth, height)
if (isSatisfiedBy(size)) {
return size
}
}
return IntSize.Zero
}

private fun Constraints.tryMinHeight(): IntSize {
val minHeight = this.minHeight
val width = (minHeight * aspectRatio).roundToInt()
if (width > 0) {
val size = IntSize(width, minHeight)
if (isSatisfiedBy(size)) {
return size
}
}
return IntSize.Zero
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
val otherModifier = other as? ConstrainedAspectRatioModifier ?: return false
return aspectRatio == otherModifier.aspectRatio &&
matchHeightConstraintsFirst == other.matchHeightConstraintsFirst
}

override fun hashCode(): Int =
aspectRatio.hashCode() * 31 + matchHeightConstraintsFirst.hashCode()

override fun toString(): String = "ConstrainedAspectRatioModifier(aspectRatio=$aspectRatio)"
}
17 changes: 14 additions & 3 deletions ui/src/main/java/com/theoplayer/android/ui/UIController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ fun UIController(
)
)

PlayerContainer(modifier = modifier, theoplayerView = player.theoplayerView) {
PlayerContainer(modifier = modifier, player = player) {
CompositionLocalProvider(LocalPlayer provides player) {
AnimatedContent(
modifier = Modifier
Expand Down Expand Up @@ -291,13 +291,14 @@ private sealed class UIState {
@Composable
private fun PlayerContainer(
modifier: Modifier = Modifier,
theoplayerView: THEOplayerView? = null,
player: Player,
ui: @Composable () -> Unit
) {
val theoplayerView = player.theoplayerView
val containerModifier = Modifier
.fillMaxSize()
.background(Color.Black)
.then(modifier)
.playerAspectRatio(player)
if (theoplayerView == null) {
Box(
modifier = containerModifier
Expand Down Expand Up @@ -463,3 +464,13 @@ internal fun rememberTHEOplayerView(config: THEOplayerConfig? = null): THEOplaye

return theoplayerView
}

internal fun Modifier.playerAspectRatio(player: Player): Modifier {
return this.constrainedAspectRatio(
if (player.videoWidth != 0 && player.videoHeight != 0) {
player.videoWidth.toFloat() / player.videoHeight.toFloat()
} else {
16f / 9f
}
)
}

0 comments on commit a9c4724

Please sign in to comment.