From a23d9a3c3118f3aab050b47f6f2ce9d720dadc72 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:54:17 +0000 Subject: [PATCH] replace the play arrow with an animated bar indicator Three equaliser bars wave with staggered phases to draw the eye to the playing track, keeping the soft shadow over cover art and flat bars in the queue. --- .../com/subtracks/ui/library/SongsContent.kt | 90 ++++++++++--------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt index 7ab37a12..96cce4a6 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt @@ -1,5 +1,10 @@ package com.subtracks.ui.library +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -13,7 +18,6 @@ import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.DownloadDone -import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -21,6 +25,7 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -36,13 +41,8 @@ import androidx.compose.ui.graphics.asAndroidPath import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.drawIntoCanvas -import androidx.compose.ui.graphics.drawscope.withTransform import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.PathParser -import androidx.compose.ui.graphics.vector.VectorGroup -import androidx.compose.ui.graphics.vector.VectorPath import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -56,6 +56,8 @@ import com.subtracks.data.model.DownloadStatus import com.subtracks.data.model.Song import com.subtracks.data.model.SongDownload import com.subtracks.ui.components.CoverArt +import kotlin.math.PI +import kotlin.math.sin private val TRACK_COLUMN_WIDTH = 24.dp private val TIME_COLUMN_WIDTH = 36.dp @@ -66,23 +68,9 @@ private val PLAY_SHADOW_RADIUS = 3.dp private val PLAY_SHADOW = Color.Black.copy(alpha = 0.6f) private val INNER_SHADOW_RADIUS = 8.dp private val INNER_SHADOW = Color.Black.copy(alpha = 0.85f) -private val PLAY_ARROW = Icons.Rounded.PlayArrow -private val PLAY_ARROW_PATH: Path by lazy { buildVectorPath(PLAY_ARROW) } - -private fun buildVectorPath(vector: ImageVector): Path { - val parser = PathParser() - - fun collect(group: VectorGroup) { - for (node in group) { - when (node) { - is VectorPath -> parser.addPathNodes(node.pathData) - is VectorGroup -> collect(node) - } - } - } - collect(vector.root) - return parser.toPath() -} +private const val BAR_COUNT = 3 +private const val PLAY_BAR_PERIOD_MS = 900 +private const val PLAY_BAR_PHASE = 0.18f @Composable fun SongRow( @@ -216,30 +204,44 @@ private fun PlayIndicator( shadow: Boolean, modifier: Modifier = Modifier, ) { + val phase by + rememberInfiniteTransition(label = "playing") + .animateFloat( + initialValue = 0f, + targetValue = 1f, + animationSpec = infiniteRepeatable(tween(PLAY_BAR_PERIOD_MS, easing = LinearEasing)), + label = "phase", + ) val tint = MaterialTheme.colorScheme.primary - if (shadow) { - val radius = with(LocalDensity.current) { PLAY_SHADOW_RADIUS.toPx() } - Canvas(modifier.semantics { contentDescription = "Playing" }) { - val scale = minOf(size.width / PLAY_ARROW.viewportWidth, size.height / PLAY_ARROW.viewportHeight) - withTransform({ scale(scale, scale, pivot = Offset.Zero) }) { - drawIntoCanvas { canvas -> - val paint = - android.graphics.Paint().apply { - isAntiAlias = true - color = tint.toArgb() - setShadowLayer(radius, 0f, 0f, PLAY_SHADOW.toArgb()) - } - canvas.nativeCanvas.drawPath(PLAY_ARROW_PATH.asAndroidPath(), paint) - } + val shadowRadius = with(LocalDensity.current) { PLAY_SHADOW_RADIUS.toPx() } + Canvas(modifier.semantics { contentDescription = "Playing" }) { + val barWidth = size.width * 0.22f + val gap = size.width * 0.11f + val radius = CornerRadius(barWidth / 2f) + val total = barWidth * BAR_COUNT + gap * (BAR_COUNT - 1) + val startX = (size.width - total) / 2f + val minHeight = size.height * 0.3f + val bars = + (0 until BAR_COUNT).map { index -> + val wave = ((sin((phase + index * PLAY_BAR_PHASE) * 2.0 * PI) + 1.0) / 2.0).toFloat() + val height = minHeight + (size.height - minHeight) * wave + val x = startX + index * (barWidth + gap) + Rect(x, (size.height - height) / 2f, x + barWidth, (size.height + height) / 2f) } + if (shadow) { + val path = Path().apply { bars.forEach { addRoundRect(RoundRect(it, radius)) } } + drawIntoCanvas { canvas -> + val paint = + android.graphics.Paint().apply { + isAntiAlias = true + color = tint.toArgb() + setShadowLayer(shadowRadius, 0f, 0f, PLAY_SHADOW.toArgb()) + } + canvas.nativeCanvas.drawPath(path.asAndroidPath(), paint) + } + } else { + bars.forEach { drawRoundRect(color = tint, topLeft = it.topLeft, size = it.size, cornerRadius = radius) } } - } else { - Icon( - imageVector = PLAY_ARROW, - contentDescription = "Playing", - tint = tint, - modifier = modifier, - ) } }