feat(player): 3D-переворот обложки при смене трека
Вместо простой смены — эффект переворота (как страница альбома/пластинка): старая обложка улетает передней гранью (0–90°), новая прилетает задней (90–180°, контр-вращение чтобы не зеркалилась). Компонент FlipCover, подключён к обложке в плеере; срабатывает при смене coverUrl трека. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
78
app/src/main/java/com/radiola/ui/components/FlipCover.kt
Normal file
78
app/src/main/java/com/radiola/ui/components/FlipCover.kt
Normal file
@@ -0,0 +1,78 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user