mirror of
https://github.com/FluxaMedia/fluxa.git
synced 2026-08-18 04:55:58 +00:00
Stagger TV hero content reveal and consolidate motion durations
Billboard rotations now play one orchestrated moment: logo, metadata, description, and buttons fade and rise in sequence, driven by a single Animatable read inside graphicsLayer so the stagger never recomposes the panel. Ad-hoc tween literals across hero, shelves, cards, and the ambient tint move to FluxaDimensions.AnimDuration tokens; the hero title fallback also picks up the display face.
This commit is contained in:
parent
327e869488
commit
f4f233f59d
5 changed files with 43 additions and 15 deletions
|
|
@ -77,7 +77,6 @@ object FluxaDimensions {
|
|||
// ── Profile screens ───────────────────────────────────────────────────────
|
||||
object Profile {
|
||||
val avatarSize = 120.dp
|
||||
val avatarPickerThumbSize = 80.dp
|
||||
}
|
||||
|
||||
// ── Card text sizes ───────────────────────────────────────────────────────
|
||||
|
|
@ -111,6 +110,10 @@ object FluxaDimensions {
|
|||
const val heightAnim = 130
|
||||
const val settingsExpand = 240
|
||||
const val settingsExpandAlt = 260
|
||||
const val heroReveal = 650
|
||||
const val fadeIn = 200
|
||||
const val fadeOut = 150
|
||||
const val ambientColor = 700
|
||||
}
|
||||
|
||||
// ── Focus indicator stroke ────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ fun rememberAmbientHeroColor(artworkUrl: String?): Color {
|
|||
value = ThemeHelper.extractColors(context, artworkUrl)
|
||||
}
|
||||
val target = lerp(colors.darkMuted, FluxaColors.background, 0.35f)
|
||||
return animateColorAsState(target, tween(700), label = "ambientHero").value
|
||||
return animateColorAsState(target, tween(FluxaDimensions.AnimDuration.ambientColor), label = "ambientHero").value
|
||||
}
|
||||
|
||||
object ThemeHelper {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -284,6 +287,13 @@ private fun youtubeEmbedHtml(youtubeId: String): String {
|
|||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun Modifier.heroReveal(progress: () -> Float, step: Int): Modifier = graphicsLayer {
|
||||
val start = step * 0.15f
|
||||
val f = ((progress() - start) / 0.55f).coerceIn(0f, 1f)
|
||||
alpha = f
|
||||
translationY = (1f - f) * 22.dp.toPx()
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun HomeHeroPanel(
|
||||
movie: Meta,
|
||||
|
|
@ -300,12 +310,16 @@ internal fun HomeHeroPanel(
|
|||
) {
|
||||
val buttonsAlpha by animateFloatAsState(
|
||||
targetValue = if (showButtons) 1f else 0f,
|
||||
animationSpec = tween(220),
|
||||
animationSpec = tween(FluxaDimensions.AnimDuration.contentExpand),
|
||||
label = "heroButtonsAlpha"
|
||||
)
|
||||
val logoCandidates = remember(movie.id, movie.logo, logoUrl) {
|
||||
listOfNotNull(logoUrl, movie.logo).distinct()
|
||||
}
|
||||
val reveal = remember(movie.id) { Animatable(0f) }
|
||||
LaunchedEffect(movie.id) {
|
||||
reveal.animateTo(1f, tween(FluxaDimensions.AnimDuration.heroReveal))
|
||||
}
|
||||
|
||||
androidx.compose.foundation.layout.Column(modifier = modifier.widthIn(max = 560.dp)) {
|
||||
if (logoCandidates.isNotEmpty()) {
|
||||
|
|
@ -314,7 +328,8 @@ internal fun HomeHeroPanel(
|
|||
contentDescription = movie.name,
|
||||
modifier = Modifier
|
||||
.height(94.dp)
|
||||
.widthIn(max = 360.dp),
|
||||
.widthIn(max = 360.dp)
|
||||
.heroReveal({ reveal.value }, 0),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
} else {
|
||||
|
|
@ -323,20 +338,26 @@ internal fun HomeHeroPanel(
|
|||
color = Color(0xFFFFD94B),
|
||||
fontSize = 54.sp,
|
||||
lineHeight = 56.sp,
|
||||
fontFamily = FluxaDisplay,
|
||||
fontWeight = FontWeight.Black,
|
||||
style = TextStyle(
|
||||
shadow = Shadow(
|
||||
color = Color.Black.copy(alpha = 0.55f),
|
||||
blurRadius = 20f
|
||||
)
|
||||
)
|
||||
),
|
||||
modifier = Modifier.heroReveal({ reveal.value }, 0)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
HeroMetaLine(movie = movie, lang = lang)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
HeroRatingLine(movie = movie)
|
||||
Box(modifier = Modifier.heroReveal({ reveal.value }, 1)) {
|
||||
androidx.compose.foundation.layout.Column {
|
||||
HeroMetaLine(movie = movie, lang = lang)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
HeroRatingLine(movie = movie)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(18.dp))
|
||||
|
||||
if (!movie.description.isNullOrBlank()) {
|
||||
|
|
@ -347,14 +368,18 @@ internal fun HomeHeroPanel(
|
|||
lineHeight = 27.sp,
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.widthIn(max = 500.dp)
|
||||
modifier = Modifier
|
||||
.widthIn(max = 500.dp)
|
||||
.heroReveal({ reveal.value }, 2)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier.alpha(buttonsAlpha)
|
||||
modifier = Modifier
|
||||
.alpha(buttonsAlpha)
|
||||
.heroReveal({ reveal.value }, 3)
|
||||
) {
|
||||
Button(
|
||||
onClick = onPlayClick,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ private fun WideContinueCard(
|
|||
onFocus: () -> Unit
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(if (isFocused) 1.035f else 1f, tween(180), label = "continueScale")
|
||||
val scale by animateFloatAsState(if (isFocused) 1.035f else 1f, tween(FluxaDimensions.AnimDuration.scaleAlpha), label = "continueScale")
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -353,12 +353,12 @@ internal fun HomeShelfRow(
|
|||
|
||||
@Composable
|
||||
private fun ShelfExpandedDescription(expandedMeta: Meta?, expandedOffsetX: Dp, rowWidth: Dp) {
|
||||
val animatedOffsetX by animateDpAsState(expandedOffsetX, tween(220), label = "expandedInfoOffset")
|
||||
val animatedOffsetX by animateDpAsState(expandedOffsetX, tween(FluxaDimensions.AnimDuration.contentExpand), label = "expandedInfoOffset")
|
||||
Box(modifier = Modifier.fillMaxWidth().height(90.dp)) {
|
||||
AnimatedVisibility(
|
||||
visible = expandedMeta != null,
|
||||
enter = fadeIn(tween(200)),
|
||||
exit = fadeOut(tween(150))
|
||||
enter = fadeIn(tween(FluxaDimensions.AnimDuration.fadeIn)),
|
||||
exit = fadeOut(tween(FluxaDimensions.AnimDuration.fadeOut))
|
||||
) {
|
||||
val info = expandedMeta
|
||||
if (info != null) {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ internal fun TvMovieCard(
|
|||
val radius = posterCornerRadius(profile?.safeCardCornerPreset ?: "soft")
|
||||
val animationDuration = when {
|
||||
profile?.safeAnimationsEnabled == false -> 0
|
||||
else -> if (isShelfStyle) 220 else 240
|
||||
else -> if (isShelfStyle) FluxaDimensions.AnimDuration.contentExpand else FluxaDimensions.AnimDuration.cardFocusScale
|
||||
}
|
||||
val hidePosterTitles = profile?.safePosterHideTitles == true || meta.hideTitle == true
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue