Вместо простой смены — эффект переворота (как страница альбома/пластинка): старая обложка улетает передней гранью (0–90°), новая прилетает задней (90–180°, контр-вращение чтобы не зеркалилась). Компонент FlipCover, подключён к обложке в плеере; срабатывает при смене coverUrl трека. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
79 lines
2.9 KiB
Kotlin
79 lines
2.9 KiB
Kotlin
package com.radiola.ui.components
|
||
|
||
import androidx.compose.animation.core.Animatable
|
||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||
import androidx.compose.animation.core.tween
|
||
import androidx.compose.foundation.layout.Box
|
||
import androidx.compose.foundation.layout.fillMaxSize
|
||
import androidx.compose.runtime.Composable
|
||
import androidx.compose.runtime.LaunchedEffect
|
||
import androidx.compose.runtime.getValue
|
||
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.graphics.graphicsLayer
|
||
import androidx.compose.ui.layout.ContentScale
|
||
import coil.compose.AsyncImage
|
||
|
||
/**
|
||
* Обложка с эффектом 3D-переворота при смене изображения — как будто
|
||
* перелистывается страница альбома / пластинка. Старая обложка «улетает»
|
||
* передней стороной (0–90°), новая «прилетает» задней (90–180°).
|
||
*/
|
||
@Composable
|
||
fun FlipCover(
|
||
model: String?,
|
||
contentDescription: String?,
|
||
modifier: Modifier = Modifier,
|
||
fallback: @Composable () -> Unit,
|
||
) {
|
||
var current by remember { mutableStateOf(model) }
|
||
var previous by remember { mutableStateOf(model) }
|
||
val rotation = remember { Animatable(0f) }
|
||
|
||
LaunchedEffect(model) {
|
||
if (model != current) {
|
||
previous = current
|
||
current = model
|
||
rotation.snapTo(0f)
|
||
rotation.animateTo(180f, animationSpec = tween(620, easing = FastOutSlowInEasing))
|
||
// Оседаем: новая обложка становится «лицом», угол 0 — без рывка.
|
||
previous = current
|
||
rotation.snapTo(0f)
|
||
}
|
||
}
|
||
|
||
val angle = rotation.value
|
||
val showFront = angle <= 90f
|
||
val faceModel = if (showFront) previous else current
|
||
|
||
Box(
|
||
modifier = modifier.graphicsLayer {
|
||
rotationY = angle
|
||
cameraDistance = 16f * density
|
||
},
|
||
contentAlignment = Alignment.Center,
|
||
) {
|
||
// Заднюю грань контр-вращаем, чтобы изображение не было зеркальным.
|
||
Box(
|
||
modifier = Modifier
|
||
.fillMaxSize()
|
||
.graphicsLayer { rotationY = if (showFront) 0f else 180f },
|
||
contentAlignment = Alignment.Center,
|
||
) {
|
||
if (!faceModel.isNullOrBlank()) {
|
||
AsyncImage(
|
||
model = faceModel,
|
||
contentDescription = contentDescription,
|
||
modifier = Modifier.fillMaxSize(),
|
||
contentScale = ContentScale.Crop,
|
||
)
|
||
} else {
|
||
fallback()
|
||
}
|
||
}
|
||
}
|
||
}
|