diff --git a/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt b/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt index 6bee059f67..12ed29425e 100644 --- a/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt +++ b/demos/compose/src/main/java/androidx/media3/demo/compose/MainActivity.kt @@ -21,6 +21,9 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -30,15 +33,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.ScaleFactor +import androidx.compose.ui.layout.lerp import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LifecycleResumeEffect @@ -124,7 +132,7 @@ private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) { player = player, surfaceType = SURFACE_TYPE_SURFACE_VIEW, modifier = Modifier.noRippleClickable { showControls = !showControls }, - contentScale = contentScales[currentContentScaleIndex].second + contentScale = contentScales[currentContentScaleIndex].second.interpolate() ) if (!renderingState.renderedFirstFrame) { // hide the surface that is being prepared behind a scrim @@ -147,3 +155,87 @@ private fun MediaPlayerScreen(player: Player, modifier: Modifier = Modifier) { } } } + + + +@Composable +private fun ContentScale.interpolate(): ContentScale { + var interpolation by remember { + mutableFloatStateOf(1f) + } + var previousScale by remember { + mutableStateOf(this) + } + val currentScale by remember { + mutableStateOf(this) + }.apply { + if (value != this@interpolate) previousScale = when { + interpolation == 1f -> value + else -> CapturedContentScale( + capturedInterpolation = interpolation, + previousScale = previousScale, + currentScale = value + ) + }.also { interpolation = 0f } + value = this@interpolate + } + + LaunchedEffect(currentScale) { + animate( + initialValue = 0f, + targetValue = 1f, + animationSpec = spring( + stiffness = Spring.StiffnessMedium + ), + block = { progress, _ -> + interpolation = progress + }, + ) + } + + return remember { + object : ContentScale { + override fun computeScaleFactor( + srcSize: Size, + dstSize: Size + ): ScaleFactor { + val start = previousScale.computeScaleFactor( + srcSize = srcSize, + dstSize = dstSize + ) + val stop = currentScale.computeScaleFactor( + srcSize = srcSize, + dstSize = dstSize + ) + return if (start == stop) stop + else lerp( + start = start, + stop = stop, + fraction = interpolation + ) + } + } + } +} + +private class CapturedContentScale( + private val capturedInterpolation: Float, + private val previousScale: ContentScale, + private val currentScale: ContentScale, +) : ContentScale { + + override fun computeScaleFactor( + srcSize: Size, + dstSize: Size + ): ScaleFactor = lerp( + start = previousScale.computeScaleFactor( + srcSize = srcSize, + dstSize = dstSize + ), + stop = currentScale.computeScaleFactor( + srcSize = srcSize, + dstSize = dstSize + ), + fraction = capturedInterpolation + ) +} \ No newline at end of file