From 76f828c6bb26ad689070a671307d828b3cbde964 Mon Sep 17 00:00:00 2001
From: Mattias Buelens <649348+MattiasBuelens@users.noreply.github.com>
Date: Tue, 10 Sep 2024 11:08:20 +0200
Subject: [PATCH] Update Jetpack Compose to 2024.09.00 (#39)
* Update Compose dependencies
* Restore look-and-feel of SeekBar
* Use new ripple API
* Remove obsolete workaround
* Fix indentation
* Rework IconButton
* Switch to androidx.lifecycle.compose.LocalLifecycleOwner
* Update changelog
* Update to AGP 8.5.2
* Fix text alignment in SeekButton
* Support API level 21
---
.idea/kotlinc.xml | 2 +-
CHANGELOG.md | 6 +
app/build.gradle.kts | 6 +-
gradle/libs.versions.toml | 14 +-
gradle/wrapper/gradle-wrapper.properties | 2 +-
ui/build.gradle.kts | 6 +-
.../com/theoplayer/android/ui/ErrorDisplay.kt | 6 +-
.../com/theoplayer/android/ui/IconButton.kt | 87 ++++++------
.../com/theoplayer/android/ui/LiveButton.kt | 6 +-
.../java/com/theoplayer/android/ui/SeekBar.kt | 130 +++++++++++++++---
.../com/theoplayer/android/ui/SeekButton.kt | 4 +-
.../com/theoplayer/android/ui/UIController.kt | 2 +-
12 files changed, 183 insertions(+), 88 deletions(-)
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 0fc3113..4cb7457 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,6 +1,6 @@
-
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 153c7b6..7c9b9ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,12 @@
> - 🏠 Internal
> - 💅 Polish
+## Unreleased
+
+* 💥 Updated to Jetpack Compose version 1.7.0 ([BOM](https://developer.android.com/jetpack/compose/bom) 2024.09.00).
+* 💥 Changed `colors` parameter in `IconButton` and `LiveButton` to be an `IconButtonColors`.
+* 🚀 Added support for Android Lollipop (API 21), to align with the THEOplayer Android SDK.
+
## v1.8.0 (2024-09-06)
* 🚀 Added support for THEOplayer 8.0.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 50f952d..d9f99cf 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,7 +9,7 @@ android {
defaultConfig {
applicationId = "com.theoplayer.android.ui.demo"
- minSdk = 24
+ minSdk = 21
targetSdk = 33
versionCode = 1
versionName = "1.0"
@@ -45,7 +45,7 @@ android {
compose = true
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.4.3"
+ kotlinCompilerExtensionVersion = "1.5.15"
}
packaging {
resources {
@@ -58,7 +58,7 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ktx)
- implementation(libs.androidx.lifecycle.runtime)
+ implementation(libs.androidx.lifecycle.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.ui.ui)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4438cf1..565dd90 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,14 +1,14 @@
[versions]
-gradle = "8.3.2"
-kotlin-gradle-plugin = "1.8.10"
+gradle = "8.5.2"
+kotlin-gradle-plugin = "1.9.25"
ktx = "1.13.1"
-lifecycle-runtime = "2.8.4"
-activity-compose = "1.9.1"
+lifecycle-compose = "2.8.5"
+activity-compose = "1.9.2"
appcompat = "1.7.0"
-compose-bom = "2024.06.00"
+compose-bom = "2024.09.00"
junit4 = "4.13.2"
playServices-castFramework = "21.5.0"
-ui-test-junit4 = "1.6.8" # ...not in BOM for some reason?
+ui-test-junit4 = "1.7.0" # ...not in BOM for some reason?
androidx-junit = "1.2.1"
androidx-espresso = "3.6.1"
androidx-mediarouter = "1.7.0"
@@ -17,7 +17,7 @@ theoplayer = "7.11.0"
[libraries]
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }
-androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime" }
+androidx-lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle-compose" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b3f1727..544b5ee 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Mon Nov 20 16:01:06 CET 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
index 3d3877f..5063c67 100644
--- a/ui/build.gradle.kts
+++ b/ui/build.gradle.kts
@@ -23,7 +23,7 @@ android {
compileSdk = 34
defaultConfig {
- minSdk = 24
+ minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -51,7 +51,7 @@ android {
compose = true
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.4.3"
+ kotlinCompilerExtensionVersion = "1.5.15"
}
packaging {
resources {
@@ -71,7 +71,7 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ktx)
- implementation(libs.androidx.lifecycle.runtime)
+ implementation(libs.androidx.lifecycle.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.ui.ui)
diff --git a/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt b/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt
index de7c4fb..edcedd4 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/ErrorDisplay.kt
@@ -25,9 +25,7 @@ import androidx.compose.ui.unit.dp
fun ErrorDisplay(
modifier: Modifier = Modifier,
) {
- val error = Player.current?.error
-
- error?.let { it ->
+ Player.current?.error?.let { error ->
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
@@ -50,7 +48,7 @@ fun ErrorDisplay(
)
}
Text(
- text = "${it.message}"
+ text = "${error.message}"
)
}
}
diff --git a/ui/src/main/java/com/theoplayer/android/ui/IconButton.kt b/ui/src/main/java/com/theoplayer/android/ui/IconButton.kt
index 2e246e7..eadd5e6 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/IconButton.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/IconButton.kt
@@ -1,19 +1,29 @@
package com.theoplayer.android.ui
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonColors
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.IconButton
+import androidx.compose.material3.IconButtonColors
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.TextButton
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
/**
@@ -40,57 +50,46 @@ fun IconButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
- colors: ButtonColors = IconButtonDefaults.iconButtonColors(),
+ colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
contentPadding: PaddingValues = PaddingValues(0.dp),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit
) {
- TextButton(
+ Box(
modifier = modifier
+ .minimumInteractiveComponentSize()
.defaultMinSize(
minWidth = IconButtonSize,
minHeight = IconButtonSize
+ )
+ .padding(contentPadding)
+ .clip(CircleShape)
+ .background(color = colors.containerColor(enabled))
+ .clickable(
+ onClick = onClick,
+ enabled = enabled,
+ role = Role.Button,
+ interactionSource = interactionSource,
+ indication = ripple(bounded = false)
),
- shape = androidx.compose.material3.IconButtonDefaults.filledShape,
- enabled = enabled,
- colors = colors,
- contentPadding = contentPadding,
- interactionSource = interactionSource,
- onClick = onClick,
- content = { content() }
- )
+ contentAlignment = Alignment.Center
+ ) {
+ val contentColor = colors.contentColor(enabled)
+ CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
+ }
}
-private const val DisabledIconOpacity = 0.38f
private val IconButtonSize = 40.dp
-/**
- * Contains the default values used by icon buttons.
- */
-object IconButtonDefaults {
- /**
- * Creates a [ButtonColors] that represents the default colors used in a [IconButton].
- *
- * Equivalent to [androidx.compose.material3.IconButtonDefaults.iconButtonColors],
- * but as [ButtonColors] instead of [androidx.compose.material3.IconButtonColors].
- *
- * @param containerColor the container color of this icon button when enabled.
- * @param contentColor the content color of this icon button when enabled.
- * @param disabledContainerColor the container color of this icon button when not enabled.
- * @param disabledContentColor the content color of this icon button when not enabled.
- */
- @Composable
- fun iconButtonColors(
- containerColor: Color = Color.Transparent,
- contentColor: Color = LocalContentColor.current,
- disabledContainerColor: Color = Color.Transparent,
- disabledContentColor: Color = contentColor.copy(alpha = DisabledIconOpacity)
- ): ButtonColors {
- return ButtonDefaults.textButtonColors(
- containerColor = containerColor,
- contentColor = contentColor,
- disabledContainerColor = disabledContainerColor,
- disabledContentColor = disabledContentColor
- )
- }
-}
\ No newline at end of file
+private fun IconButtonColors.containerColor(enabled: Boolean): Color =
+ if (enabled) containerColor else disabledContainerColor
+
+private fun IconButtonColors.contentColor(enabled: Boolean): Color =
+ if (enabled) contentColor else disabledContentColor
+
+internal fun IconButtonColors.toButtonColors() = ButtonColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor
+)
diff --git a/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt b/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt
index bc1ff90..ca1d1c3 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/LiveButton.kt
@@ -8,6 +8,8 @@ import androidx.compose.material.icons.rounded.Circle
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonColors
+import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -35,7 +37,7 @@ import com.theoplayer.android.ui.theme.THEOplayerTheme
fun LiveButton(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding,
- colors: ButtonColors = IconButtonDefaults.iconButtonColors(),
+ colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
liveThreshold: Double = 10.0,
live: @Composable RowScope.() -> Unit = {
Icon(
@@ -63,7 +65,7 @@ fun LiveButton(
TextButton(
modifier = modifier,
contentPadding = contentPadding,
- colors = colors,
+ colors = colors.toButtonColors(),
onClick = {
player.player?.let {
it.currentTime = Double.POSITIVE_INFINITY
diff --git a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt
index bc5cb08..d37e4ce 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt
@@ -1,15 +1,34 @@
package com.theoplayer.android.ui
+import androidx.compose.foundation.background
+import androidx.compose.foundation.hoverable
+import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.DragInteraction
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.systemGestureExclusion
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderColors
import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
import com.theoplayer.android.api.cast.chromecast.PlayerCastState
/**
@@ -21,6 +40,7 @@ import com.theoplayer.android.api.cast.chromecast.PlayerCastState
* @param colors [SliderColors] that will be used to resolve the colors used for this seek bar in
* different states. See [SliderDefaults.colors].
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SeekBar(
modifier: Modifier = Modifier,
@@ -46,35 +66,105 @@ fun SeekBar(
var seekTime by remember { mutableStateOf(null) }
var wasPlayingBeforeSeek by remember { mutableStateOf(false) }
+ val interactionSource = remember { MutableInteractionSource() }
+
Slider(
modifier = modifier.systemGestureExclusion(),
colors = colors,
value = seekTime ?: currentTime,
valueRange = valueRange,
enabled = enabled,
- onValueChange = remember {
- { time ->
- seekTime = time
- player?.player?.let {
- if (!it.isPaused) {
- wasPlayingBeforeSeek = true
- it.pause()
- }
- it.currentTime = time.toDouble()
+ interactionSource = interactionSource,
+ thumb = {
+ SeekBarThumb(
+ interactionSource = interactionSource,
+ colors = colors,
+ enabled = enabled
+ )
+ },
+ track = { sliderState ->
+ SliderDefaults.Track(
+ modifier = Modifier.height(4.dp),
+ colors = colors,
+ enabled = enabled,
+ sliderState = sliderState,
+ // Don't draw the stop indicator at the end of the track
+ drawStopIndicator = {},
+ // Remove the gap in the track around the thumb
+ thumbTrackGapSize = 0.dp
+ )
+ },
+ onValueChange = { time ->
+ seekTime = time
+ player?.player?.let {
+ if (!it.isPaused) {
+ wasPlayingBeforeSeek = true
+ it.pause()
}
+ it.currentTime = time.toDouble()
}
},
- // This needs to always be the *same* callback,
- // otherwise Slider will reset its internal SliderState while dragging.
- // https://github.com/androidx/androidx/blob/4d69c45e6361a2e5af77edc9f7f92af3d0db3877/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt#L270-L282
- onValueChangeFinished = remember {
- {
- seekTime = null
- if (wasPlayingBeforeSeek) {
- player?.player?.play()
- wasPlayingBeforeSeek = false
- }
+ onValueChangeFinished = {
+ seekTime = null
+ if (wasPlayingBeforeSeek) {
+ player?.player?.play()
+ wasPlayingBeforeSeek = false
}
}
)
-}
\ No newline at end of file
+}
+
+private val ThumbSize = DpSize(20.dp, 20.dp)
+private val ThumbDefaultElevation = 1.dp
+private val ThumbPressedElevation = 6.dp
+private val StateLayerSize = 40.0.dp
+
+// Slider.Thumb look-and-feel from Compose Material3 version 1.2.1
+// https://github.com/androidx/androidx/blob/d4655d87a9f8dbced1c3c768a595cbfcea505c07/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Slider.kt#L980
+@Composable
+private fun SeekBarThumb(
+ interactionSource: MutableInteractionSource,
+ modifier: Modifier = Modifier,
+ colors: SliderColors = SliderDefaults.colors(),
+ enabled: Boolean = true,
+ thumbSize: DpSize = ThumbSize
+) {
+ val interactions = remember { mutableStateListOf() }
+ LaunchedEffect(interactionSource) {
+ interactionSource.interactions.collect { interaction ->
+ when (interaction) {
+ is PressInteraction.Press -> interactions.add(interaction)
+ is PressInteraction.Release -> interactions.remove(interaction.press)
+ is PressInteraction.Cancel -> interactions.remove(interaction.press)
+ is DragInteraction.Start -> interactions.add(interaction)
+ is DragInteraction.Stop -> interactions.remove(interaction.start)
+ is DragInteraction.Cancel -> interactions.remove(interaction.start)
+ }
+ }
+ }
+
+ val elevation = if (interactions.isNotEmpty()) {
+ ThumbPressedElevation
+ } else {
+ ThumbDefaultElevation
+ }
+ val shape = CircleShape
+
+ Spacer(
+ modifier
+ .size(thumbSize)
+ .indication(
+ interactionSource = interactionSource,
+ indication = ripple(
+ bounded = false,
+ radius = StateLayerSize / 2
+ )
+ )
+ .hoverable(interactionSource = interactionSource)
+ .shadow(if (enabled) elevation else 0.dp, shape, clip = false)
+ .background(colors.thumbColor(enabled), shape)
+ )
+}
+
+private fun SliderColors.thumbColor(enabled: Boolean): Color =
+ if (enabled) thumbColor else disabledThumbColor
\ No newline at end of file
diff --git a/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt b/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt
index 8674b45..209d77f 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/SeekButton.kt
@@ -57,8 +57,8 @@ fun SeekButton(
)
Text(
modifier = Modifier
- .align(Alignment.TopCenter)
- .offset(y = iconSize * 0.4f),
+ .align(Alignment.Center)
+ .offset(y = iconSize * 0.1f),
text = "${seekOffset.absoluteValue}",
fontSize = 6.sp * (iconSize / 24.dp)
)
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 1f78ef1..9e2e989 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt
@@ -41,11 +41,11 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
import com.theoplayer.android.api.THEOplayerConfig
import com.theoplayer.android.api.THEOplayerView
import com.theoplayer.android.api.cast.chromecast.PlayerCastState