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