feat(player): enhance UI responsiveness with animated color states for buttons and track selection

This commit is contained in:
2026-05-09 08:49:49 +02:00
parent 4e322e7723
commit 4e9d8c0b3b
3 changed files with 209 additions and 125 deletions

View File

@@ -54,6 +54,18 @@ internal fun TvIconButton(
targetValue = if (isFocused) scheme.primary else Color.Transparent,
label = "border"
)
val backgroundColor by animateColorAsState(
targetValue = if (isFocused) {
scheme.primaryContainer
} else {
scheme.surfaceContainerHigh.copy(alpha = 0.72f)
},
label = "background"
)
val contentColor by animateColorAsState(
targetValue = if (isFocused) scheme.onPrimaryContainer else scheme.onSurface,
label = "content"
)
val shape = RoundedCornerShape(50)
val iconSize = (size - 24).coerceAtLeast(0).dp
@@ -69,10 +81,7 @@ internal fun TvIconButton(
shape = shape
)
.clip(shape)
.background(
if (isFocused) scheme.primary.copy(alpha = 0.5f)
else scheme.background.copy(alpha = 0.65f)
)
.background(backgroundColor)
.focusProperties { canFocus = enabled }
.semantics {
if (!enabled) {
@@ -86,7 +95,7 @@ internal fun TvIconButton(
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = scheme.onBackground,
tint = contentColor,
modifier = buttonModifier
.size(size.dp)
.padding(12.dp)
@@ -104,12 +113,12 @@ internal fun TvIconButton(
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = scheme.onBackground,
tint = contentColor,
modifier = Modifier.size(iconSize)
)
Text(
text = label,
color = scheme.onBackground,
color = contentColor,
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold
)

View File

@@ -1,5 +1,7 @@
package hu.bbara.purefin.ui.screen.player.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -7,14 +9,19 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -25,11 +32,14 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
@@ -82,24 +92,6 @@ internal fun TvPlayerQueuePanel(
shape = RoundedCornerShape(24.dp),
color = scheme.surface.copy(alpha = 0.94f)
) {
Column(
modifier = Modifier.padding(horizontal = 22.dp, vertical = 20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = "Playlist",
color = scheme.onSurface,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = queueCountLabel,
color = scheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
}
if (uiState.queue.isEmpty()) {
Text(
text = "Add something to the queue to browse it here.",
@@ -113,7 +105,7 @@ internal fun TvPlayerQueuePanel(
.testTag(TvPlayerPlaylistRowTag),
state = listState,
horizontalArrangement = Arrangement.spacedBy(14.dp),
contentPadding = PaddingValues(end = 4.dp)
contentPadding = PaddingValues(16.dp)
) {
itemsIndexed(uiState.queue, key = { _, item -> item.id }) { index, item ->
val isEntryItem = index == entryIndex
@@ -154,7 +146,6 @@ internal fun TvPlayerQueuePanel(
}
}
}
}
}
@Composable
@@ -169,25 +160,49 @@ private fun TvQueueRowCard(
) {
val scheme = MaterialTheme.colorScheme
var isFocused by remember { mutableStateOf(false) }
val borderColor = when {
val scale by animateFloatAsState(
targetValue = if (isFocused) 1.06f else 1f,
label = "queueCardScale"
)
val borderColor by animateColorAsState(
targetValue = when {
isFocused -> scheme.primary
isCurrent -> scheme.primary.copy(alpha = 0.55f)
else -> scheme.outlineVariant.copy(alpha = 0.35f)
}
val backgroundColor = when {
isFocused -> scheme.primary.copy(alpha = 0.18f)
isCurrent -> scheme.surfaceVariant.copy(alpha = 0.78f)
else -> scheme.surfaceVariant.copy(alpha = 0.62f)
isCurrent -> scheme.tertiary
else -> scheme.outlineVariant.copy(alpha = 0.5f)
},
label = "queueCardBorder"
)
val backgroundColor by animateColorAsState(
targetValue = when {
isFocused -> scheme.primaryContainer.copy(alpha = 0.96f)
isCurrent -> scheme.tertiaryContainer.copy(alpha = 0.86f)
else -> scheme.surfaceContainerHigh.copy(alpha = 0.72f)
},
label = "queueCardBackground"
)
val contentColor = when {
isFocused -> scheme.onPrimaryContainer
isCurrent -> scheme.onTertiaryContainer
else -> scheme.onSurface
}
val shape = RoundedCornerShape(18.dp)
Column(
modifier = modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
}
.border(
width = if (isFocused) 2.dp else 1.dp,
width = when {
isFocused -> 3.dp
isCurrent -> 2.dp
else -> 1.dp
},
color = borderColor,
shape = RoundedCornerShape(18.dp)
shape = shape
)
.clip(RoundedCornerShape(18.dp))
.clip(shape)
.background(backgroundColor)
.onFocusChanged { isFocused = it.isFocused }
.onPreviewKeyEvent { event ->
@@ -207,37 +222,34 @@ private fun TvQueueRowCard(
}
}
.clickable { onClick() }
.padding(12.dp),
.padding(4.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
TvQueueArtwork(
artworkUrl = item.artworkUrl,
isCurrent = isCurrent,
badgeColor = if (isFocused) scheme.primary else scheme.tertiary,
badgeContentColor = if (isFocused) scheme.onPrimary else scheme.onTertiary,
modifier = Modifier.fillMaxWidth()
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
if (isCurrent) {
Text(
text = "Current",
color = scheme.primary,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.SemiBold
)
}
Text(
text = item.title,
color = scheme.onSurface,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis
color = contentColor,
style = MaterialTheme.typography.bodyMedium,
fontWeight = if (isCurrent || isFocused) FontWeight.Bold else FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 8.dp)
)
}
}
}
@Composable
private fun TvQueueArtwork(
artworkUrl: String?,
isCurrent: Boolean,
badgeColor: Color,
badgeContentColor: Color,
modifier: Modifier = Modifier
) {
val scheme = MaterialTheme.colorScheme
@@ -268,5 +280,30 @@ private fun TvQueueArtwork(
.background(scheme.surfaceContainerHigh)
)
}
if (isCurrent) {
Row(
modifier = Modifier
.align(Alignment.TopStart)
.padding(8.dp)
.clip(RoundedCornerShape(50))
.background(badgeColor)
.padding(horizontal = 9.dp, vertical = 5.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.PlayArrow,
contentDescription = null,
tint = badgeContentColor,
modifier = Modifier.size(14.dp)
)
Text(
text = "Now playing",
color = badgeContentColor,
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold
)
}
}
}
}

View File

@@ -1,25 +1,30 @@
package hu.bbara.purefin.ui.screen.player.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.ClosedCaption
import androidx.compose.material.icons.outlined.HighQuality
import androidx.compose.material.icons.outlined.Language
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -211,23 +216,42 @@ private fun TvTrackOptionRow(
) {
val scheme = MaterialTheme.colorScheme
var isFocused by remember { mutableStateOf(false) }
val backgroundColor by animateColorAsState(
targetValue = when {
isFocused -> scheme.primaryContainer
selected -> scheme.secondaryContainer.copy(alpha = 0.9f)
else -> scheme.surfaceContainerHigh.copy(alpha = 0.72f)
},
label = "trackOptionBackground"
)
val contentColor by animateColorAsState(
targetValue = when {
isFocused -> scheme.onPrimaryContainer
selected -> scheme.onSecondaryContainer
else -> scheme.onSurface
},
label = "trackOptionContent"
)
val borderColor by animateColorAsState(
targetValue = when {
isFocused -> scheme.primary
selected -> scheme.secondary
else -> scheme.outlineVariant.copy(alpha = 0.45f)
},
label = "trackOptionBorder"
)
val shape = RoundedCornerShape(12.dp)
Box(
modifier = modifier
.fillMaxWidth()
.then(
if (isFocused) {
Modifier.border(2.dp, scheme.primary, RoundedCornerShape(12.dp))
} else {
Modifier
}
)
.clip(RoundedCornerShape(12.dp))
.background(
if (isFocused) scheme.primary.copy(alpha = 0.3f)
else if (selected) scheme.primary.copy(alpha = 0.15f)
else scheme.surfaceVariant.copy(alpha = 0.6f)
.border(
width = if (isFocused) 3.dp else if (selected) 2.dp else 1.dp,
color = borderColor,
shape = shape
)
.clip(shape)
.background(backgroundColor)
.onFocusChanged { isFocused = it.isFocused }
.onPreviewKeyEvent { event ->
if (event.type != KeyEventType.KeyDown) {
@@ -243,12 +267,26 @@ private fun TvTrackOptionRow(
}
.clickable { onClick() }
.padding(horizontal = 20.dp, vertical = 14.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
color = scheme.onSurface,
color = contentColor,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
fontWeight = if (selected || isFocused) FontWeight.SemiBold else FontWeight.Normal
)
if (selected) {
Icon(
imageVector = Icons.Outlined.Check,
contentDescription = null,
tint = contentColor,
modifier = Modifier.size(22.dp)
)
}
}
}
}