mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
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.
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
package com.subtracks.ui.library
|
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.Canvas
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.foundation.text.TextAutoSize
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.DownloadDone
|
import androidx.compose.material.icons.rounded.DownloadDone
|
||||||
import androidx.compose.material.icons.rounded.PlayArrow
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
@@ -21,6 +25,7 @@ import androidx.compose.material3.ListItemDefaults
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.DrawScope
|
||||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
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.nativeCanvas
|
||||||
import androidx.compose.ui.graphics.toArgb
|
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.platform.LocalDensity
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
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.Song
|
||||||
import com.subtracks.data.model.SongDownload
|
import com.subtracks.data.model.SongDownload
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
private val TRACK_COLUMN_WIDTH = 24.dp
|
private val TRACK_COLUMN_WIDTH = 24.dp
|
||||||
private val TIME_COLUMN_WIDTH = 36.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 PLAY_SHADOW = Color.Black.copy(alpha = 0.6f)
|
||||||
private val INNER_SHADOW_RADIUS = 8.dp
|
private val INNER_SHADOW_RADIUS = 8.dp
|
||||||
private val INNER_SHADOW = Color.Black.copy(alpha = 0.85f)
|
private val INNER_SHADOW = Color.Black.copy(alpha = 0.85f)
|
||||||
private val PLAY_ARROW = Icons.Rounded.PlayArrow
|
private const val BAR_COUNT = 3
|
||||||
private val PLAY_ARROW_PATH: Path by lazy { buildVectorPath(PLAY_ARROW) }
|
private const val PLAY_BAR_PERIOD_MS = 900
|
||||||
|
private const val PLAY_BAR_PHASE = 0.18f
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SongRow(
|
fun SongRow(
|
||||||
@@ -216,30 +204,44 @@ private fun PlayIndicator(
|
|||||||
shadow: Boolean,
|
shadow: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
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
|
val tint = MaterialTheme.colorScheme.primary
|
||||||
if (shadow) {
|
val shadowRadius = with(LocalDensity.current) { PLAY_SHADOW_RADIUS.toPx() }
|
||||||
val radius = with(LocalDensity.current) { PLAY_SHADOW_RADIUS.toPx() }
|
Canvas(modifier.semantics { contentDescription = "Playing" }) {
|
||||||
Canvas(modifier.semantics { contentDescription = "Playing" }) {
|
val barWidth = size.width * 0.22f
|
||||||
val scale = minOf(size.width / PLAY_ARROW.viewportWidth, size.height / PLAY_ARROW.viewportHeight)
|
val gap = size.width * 0.11f
|
||||||
withTransform({ scale(scale, scale, pivot = Offset.Zero) }) {
|
val radius = CornerRadius(barWidth / 2f)
|
||||||
drawIntoCanvas { canvas ->
|
val total = barWidth * BAR_COUNT + gap * (BAR_COUNT - 1)
|
||||||
val paint =
|
val startX = (size.width - total) / 2f
|
||||||
android.graphics.Paint().apply {
|
val minHeight = size.height * 0.3f
|
||||||
isAntiAlias = true
|
val bars =
|
||||||
color = tint.toArgb()
|
(0 until BAR_COUNT).map { index ->
|
||||||
setShadowLayer(radius, 0f, 0f, PLAY_SHADOW.toArgb())
|
val wave = ((sin((phase + index * PLAY_BAR_PHASE) * 2.0 * PI) + 1.0) / 2.0).toFloat()
|
||||||
}
|
val height = minHeight + (size.height - minHeight) * wave
|
||||||
canvas.nativeCanvas.drawPath(PLAY_ARROW_PATH.asAndroidPath(), paint)
|
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user