MovieDetailsPosterStateController.kt
package com.louisfn.somovie.feature.moviedetails.poster
import androidx.annotation.AnyThread
import androidx.annotation.UiThread
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
import com.louisfn.somovie.ui.common.extension.roundToPx
import com.louisfn.somovie.ui.common.extension.screenHeightWithInset
import com.louisfn.somovie.ui.common.extension.screenWidth
import com.louisfn.somovie.ui.theme.Dimens
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
private val MinDragDistanceToReduce = 96.dp
@Stable
internal class PosterStateController(
reducedCoordinates: LayoutCoordinates,
private val scope: CoroutineScope,
private val minDragDistanceToReduce: Int,
private val screenSize: Size,
private var onStateChanged: (MovieDetailsPosterState) -> Unit,
) {
private var currentState: MovieDetailsPosterState = MovieDetailsPosterState.REDUCED
private val reducedSize = reducedCoordinates.size.toSize()
private val reducedOffset = reducedCoordinates.positionInParent()
private val expandedSize = Size(width = screenSize.width, height = screenSize.width / Dimens.PosterRatio)
private val expandedOffset = Offset(x = 0f, y = screenSize.height / 2 - expandedSize.height / 2)
private var dragOffset = Offset.Zero
private val sizeAnimatable = Animatable(reducedSize, Size.VectorConverter)
private val offsetAnimatable = Animatable(reducedOffset, Offset.VectorConverter)
val size by derivedStateOf { sizeAnimatable.value }
val offset by derivedStateOf { offsetAnimatable.value }
@UiThread
fun animateToState(state: MovieDetailsPosterState) {
currentState = state
when (state) {
MovieDetailsPosterState.EXPANDED -> animateToExpandedState()
MovieDetailsPosterState.REDUCED -> animateToReducedState()
}
}
@UiThread
fun onDrag(dragAmount: Offset) {
if (currentState != MovieDetailsPosterState.EXPANDED) return
dragOffset = dragOffset.plus(dragAmount)
val newWidth =
if (dragOffset.getDistance() <= minDragDistanceToReduce) {
screenSize.width - dragOffset.getDistance()
} else {
sizeAnimatable.value.width
}
val x = (screenSize.width - newWidth) / 2
val sizeOffset = Offset(x = x, y = x / Dimens.PosterRatio)
val newOffset = expandedOffset
.plus(dragOffset)
.plus(sizeOffset)
scope.launch { offsetAnimatable.snapTo(newOffset) }
scope.launch { sizeAnimatable.snapTo(Size(width = newWidth, height = newWidth / Dimens.PosterRatio)) }
}
@UiThread
fun onDragEnd() {
if (currentState != MovieDetailsPosterState.EXPANDED) return
if (dragOffset.getDistance() > minDragDistanceToReduce) {
onStateChanged(MovieDetailsPosterState.REDUCED)
} else {
animateToExpandedState()
}
dragOffset = Offset.Zero
}
@AnyThread
private fun animateToExpandedState() {
scope.launch { offsetAnimatable.animateTo(expandedOffset) }
scope.launch { sizeAnimatable.animateTo(expandedSize) }
}
@AnyThread
private fun animateToReducedState() {
scope.launch { offsetAnimatable.animateTo(reducedOffset) }
scope.launch { sizeAnimatable.animateTo(reducedSize) }
}
}
@Composable
internal fun rememberPosterStateController(
reducedCoordinates: LayoutCoordinates,
onStateChanged: (MovieDetailsPosterState) -> Unit,
): PosterStateController {
val scope = rememberCoroutineScope()
val minDragDistanceToReduce = MinDragDistanceToReduce.roundToPx()
val screenSize = with(LocalConfiguration.current) { Size(width = screenWidth, height = screenHeightWithInset) }
return remember(reducedCoordinates) {
PosterStateController(
minDragDistanceToReduce = minDragDistanceToReduce,
reducedCoordinates = reducedCoordinates,
screenSize = screenSize,
onStateChanged = onStateChanged,
scope = scope,
)
}
}