Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.android.systemui.axdynamicbar.ui.compose

import android.graphics.drawable.GradientDrawable
import android.media.AudioManager
import android.graphics.drawable.LayerDrawable
import android.util.TypedValue
import android.widget.SeekBar
Expand Down Expand Up @@ -45,7 +46,9 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb
Expand All @@ -58,9 +61,9 @@ import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.axdynamicbar.shared.IslandActions
import com.android.systemui.axdynamicbar.model.IslandEvent
import com.android.systemui.axdynamicbar.shared.*
import com.android.systemui.media.controls.ui.drawable.SquigglyProgress
import com.android.systemui.res.R
import kotlinx.coroutines.delay
import com.android.systemui.media.controls.ui.view.WaveformSeekBar

private val AlbumArtSize = 80.dp
private val PlayPauseSize = 56.dp
Expand All @@ -78,77 +81,106 @@ internal fun MediaCard(event: IslandEvent.Media, interactor: IslandActions) {
shape = ShapeCard,
color = CardBg,
) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
Box(modifier = Modifier.fillMaxWidth()) {

event.albumArt?.let { art ->
Image(
bitmap = art.toScaledBitmap(380.dp),
contentDescription = null,
modifier = Modifier
.matchParentSize()
.blur(32.dp),
contentScale = ContentScale.Crop,
)
} ?: Box(
modifier = Modifier
.fillMaxWidth()
.clickable {
interactor.openMediaApp()
interactor.collapseIsland()
}
.padding(SpaceXxl),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(SpaceXxl),
) {
event.albumArt?.let { art ->
Image(
bitmap = art.toScaledBitmap(AlbumArtSize),
contentDescription = null,
modifier = Modifier.size(AlbumArtSize).clip(ShapeLg),
contentScale = ContentScale.Crop,
.matchParentSize()
.background(CardBg),
)

Box(
modifier = Modifier
.matchParentSize()
.background(
Brush.verticalGradient(
0.0f to Color(0xFF000000).copy(alpha = 0.4f),
0.45f to Color(0xFF000000).copy(alpha = 0.65f),
1.0f to Color(0xFF000000).copy(alpha = 1.0f),
)
)
} ?: Box(
)

Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.size(AlbumArtSize)
.clip(ShapeLg)
.background(accent.copy(alpha = AlphaFaint)),
contentAlignment = Alignment.Center,
.fillMaxWidth()
.clickable {
interactor.openMediaApp()
interactor.collapseIsland()
}
.padding(SpaceXxl),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(SpaceXxl),
) {
Icon(Icons.Filled.MusicNote, null, tint = accent, modifier = Modifier.size(36.dp))
}
event.albumArt?.let { art ->
Image(
bitmap = art.toScaledBitmap(AlbumArtSize),
contentDescription = null,
modifier = Modifier.size(AlbumArtSize).clip(ShapeLg),
contentScale = ContentScale.Crop,
)
} ?: Box(
modifier = Modifier
.size(AlbumArtSize)
.clip(ShapeLg)
.background(accent.copy(alpha = AlphaFaint)),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Filled.MusicNote, null, tint = accent, modifier = Modifier.size(36.dp))
}

Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(SpaceXs),
) {
Text(
event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) },
color = OnCardText,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
if (event.artist.isNotEmpty()) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(SpaceXs),
) {
Text(
event.artist,
color = accent,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
event.track.ifEmpty { stringResource(R.string.ax_dynamic_bar_now_playing) },
color = OnCardText,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
if (event.artist.isNotEmpty()) {
Text(
event.artist,
color = accent,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
event.appIcon?.let { icon ->
Image(
bitmap = icon.toScaledBitmap(SizeIconSm),
contentDescription = null,
modifier = Modifier.size(SizeIconSm).clip(ShapeXs),
colorFilter = ColorFilter.tint(OnCardText),
)
}
}
event.appIcon?.let { icon ->
Image(
bitmap = icon.toScaledBitmap(SizeIconSm),
contentDescription = null,
modifier = Modifier.size(SizeIconSm).clip(ShapeXs),
colorFilter = ColorFilter.tint(OnCardText),
)
}
}

Column(
modifier = Modifier
.fillMaxWidth()
.background(accent.copy(alpha = AlphaFaint))
.padding(horizontal = SpaceXxl, vertical = SpaceLg),
verticalArrangement = Arrangement.spacedBy(SpaceLg),
) {
if (event.duration > 0L) {
MediaSeekBar(event, interactor, accent)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SpaceXxl, vertical = SpaceLg),
verticalArrangement = Arrangement.spacedBy(SpaceLg),
) {
if (event.duration > 0L) {
MediaSeekBar(event, interactor, accent)
}
MediaControls(event, interactor, accent)
}
MediaControls(event, interactor, accent)
}
}
}
Expand Down Expand Up @@ -317,22 +349,16 @@ private fun MediaSeekBar(
var displayFraction by remember { mutableStateOf(serverFraction) }

val interactorRef = rememberUpdatedState(interactor)

// Read the dismiss swipe lock provided by MagneticSwipeToDismiss
val swipeLock = LocalDismissSwipeLock.current

// Smooth frame-interpolated progress when playing, snaps when paused or scrubbing
LaunchedEffect(positionMs, durationMs, isPlaying) {
if (isScrubbing) return@LaunchedEffect

displayFraction = serverFraction

if (!isPlaying || durationMs <= 0L) return@LaunchedEffect

val startWallMs = System.currentTimeMillis()
val startProgressMs = positionMs
while (true) {
delay(16L) // ~60 fps
delay(16L)
if (isScrubbing) break
val elapsed = System.currentTimeMillis() - startWallMs
val interpolated = ((startProgressMs + elapsed).toFloat() / durationMs).coerceIn(0f, 1f)
Expand All @@ -343,32 +369,25 @@ private fun MediaSeekBar(

val displayMs = (displayFraction * durationMs).toLong()
val accentArgb = accent.toArgb()
val trackAlphaArgb = accent.copy(alpha = AlphaSubtle).toArgb()

Column(verticalArrangement = Arrangement.spacedBy(SpaceXs)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
formatElapsedTime(displayMs),
color = SubtleGray,
style = MaterialTheme.typography.labelSmall,
)
Text(
formatElapsedTime(durationMs),
color = SubtleGray,
style = MaterialTheme.typography.labelSmall,
)
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(SpaceXs),
) {
Text(
formatElapsedTime(displayMs),
color = SubtleGray,
style = MaterialTheme.typography.labelSmall,
)

Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.height(SeekBarHeight)
.pointerInput(swipeLock) {
awaitEachGesture {
awaitPointerEvent() // DOWN
awaitPointerEvent()
swipeLock.value = true
try {
do {
Expand Down Expand Up @@ -408,108 +427,30 @@ private fun MediaSeekBar(
) {
AndroidView(
factory = { context ->
SeekBar(context).apply {
WaveformSeekBar(context).apply {
max = 10_000
splitTrack = false
setPadding(0, 0, 0, 0)
// Disable direct touch — Compose handles all gestures above
setWaveformColor(accentArgb)
setThumbColor(accentArgb)
isEnabled = false

// Pill-shaped thumb
thumb = createSeekBarThumb(context, accentArgb)
thumbOffset = thumb.intrinsicWidth / 2

// Set up SquigglyProgress on the progress layer
val layer = (progressDrawable?.mutate() as? LayerDrawable)
if (layer != null) {
layer.findDrawableByLayerId(android.R.id.background)
?.mutate()?.setTint(trackAlphaArgb)

layer.findDrawableByLayerId(android.R.id.secondaryProgress)
?.mutate()?.setTint(
com.android.internal.graphics.ColorUtils
.setAlphaComponent(accentArgb, 60)
)

val squiggle = SquigglyProgress().apply {
waveLength = context.resources.getDimensionPixelSize(
R.dimen.qs_media_seekbar_progress_wavelength
).toFloat()
lineAmplitude = context.resources.getDimensionPixelSize(
R.dimen.qs_media_seekbar_progress_amplitude
).toFloat()
phaseSpeed = context.resources.getDimensionPixelSize(
R.dimen.qs_media_seekbar_progress_phase
).toFloat()
strokeWidth = context.resources.getDimensionPixelSize(
R.dimen.qs_media_seekbar_progress_stroke_width
).toFloat()
setTint(accentArgb)
drawRemainingLine = false
transitionEnabled = false
animate = false
}
layer.setDrawableByLayerId(android.R.id.progress, squiggle)
progressDrawable = layer
}
}
},
update = { bar ->
val target = (displayFraction * 10_000f).toInt().coerceIn(0, 10_000)
bar.progress = target

// Re-tint thumb for accent color changes (e.g. track switch)
(bar.thumb as? GradientDrawable)?.setColor(accentArgb)

val alpha = if (isPlaying) 255 else (255 * 0.55f).toInt()
bar.thumb?.alpha = alpha

val layer = bar.progressDrawable as? LayerDrawable

// Re-tint track colors
layer?.findDrawableByLayerId(android.R.id.background)
?.setTint(trackAlphaArgb)
layer?.findDrawableByLayerId(android.R.id.secondaryProgress)
?.setTint(
com.android.internal.graphics.ColorUtils
.setAlphaComponent(accentArgb, 60)
)

val squiggle = layer
?.findDrawableByLayerId(android.R.id.progress) as? SquigglyProgress

squiggle?.apply {
setTint(accentArgb)
setAlpha(alpha)
animate = isPlaying && !isScrubbing
if (bar.progress != target) bar.progress = target
when {
isPlaying && !bar.isPlaying -> bar.startWaveAnimation()
!isPlaying && bar.isPlaying -> bar.stopWaveAnimation()
}

layer?.alpha = alpha
},
modifier = Modifier.fillMaxWidth().height(SeekBarHeight),
)
}
}
}

/**
* Creates a pill-shaped thumb drawable for the seekbar.
*/
private fun createSeekBarThumb(context: android.content.Context, tintColor: Int): GradientDrawable {
val wPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 4f, context.resources.displayMetrics
).toInt().coerceAtLeast(1)
val hPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics
).toInt().coerceAtLeast(1)
val radiusPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics
)
return GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
setSize(wPx, hPx)
cornerRadius = radiusPx
setColor(tintColor)
Text(
formatElapsedTime(durationMs),
color = SubtleGray,
style = MaterialTheme.typography.labelSmall,
)
}
}

Expand Down
Loading