diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt index c9d300bad5e0e..60ef85d0b5024 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt @@ -89,6 +89,7 @@ open class SeekBarObserver( holder.seekBar.thumb.alpha = if (data.seekAvailable) 255 else 0 holder.seekBar.isEnabled = data.seekAvailable progressDrawable?.animate = data.playing && !data.scrubbing + progressDrawable?.transitionEnabled = !data.seekAvailable if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) { holder.seekBar.maxHeight = seekBarEnabledMaxHeight diff --git a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt b/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt index fbd17d7a212ee..88f6f3dd9d0e1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SquigglyProgress.kt @@ -5,13 +5,15 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.res.ColorStateList import android.graphics.Canvas -import android.graphics.Color import android.graphics.ColorFilter import android.graphics.Paint import android.graphics.Path import android.graphics.PixelFormat import android.graphics.drawable.Drawable import android.os.SystemClock +import android.util.MathUtils.lerp +import android.util.MathUtils.lerpInv +import android.util.MathUtils.lerpInvSat import androidx.annotation.VisibleForTesting import com.android.internal.graphics.ColorUtils import com.android.systemui.animation.Interpolators @@ -34,6 +36,13 @@ class SquigglyProgress : Drawable() { private var phaseOffset = 0f private var lastFrameTime = -1L + /* distance over which amplitude drops to zero, measured in wavelengths */ + private val transitionPeriods = 1.5f + /* wave endpoint as percentage of bar when play position is zero */ + private val minWaveEndpoint = 0.2f + /* wave endpoint as percentage of bar when play position matches wave endpoint */ + private val matchedWaveEndpoint = 0.6f + // Horizontal length of the sine wave var waveLength = 0f // Height of each peak of the sine wave @@ -51,6 +60,12 @@ class SquigglyProgress : Drawable() { linePaint.strokeWidth = value } + var transitionEnabled = true + set(value) { + field = value + invalidateSelf() + } + init { wavePaint.strokeCap = Paint.Cap.ROUND linePaint.strokeCap = Paint.Cap.ROUND @@ -95,57 +110,90 @@ class SquigglyProgress : Drawable() { if (animate) { invalidateSelf() val now = SystemClock.uptimeMillis() - phaseOffset -= (now - lastFrameTime) / 1000f * phaseSpeed + phaseOffset += (now - lastFrameTime) / 1000f * phaseSpeed phaseOffset %= waveLength lastFrameTime = now } - val totalProgressPx = (bounds.width() * (level / 10_000f)) + val progress = level / 10_000f + val totalProgressPx = bounds.width() * progress + val waveProgressPx = bounds.width() * ( + if (!transitionEnabled || progress > matchedWaveEndpoint) progress else + lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress))) + + // Build Wiggly Path + val waveStart = -phaseOffset + val waveEnd = waveProgressPx + val transitionLength = if (transitionEnabled) transitionPeriods * waveLength else 0.01f + + // helper function, computes amplitude for wave segment + val computeAmplitude: (Float, Float) -> Float = { x, sign -> + sign * heightFraction * lineAmplitude * + lerpInvSat(waveEnd, waveEnd - transitionLength, x) + } + + var currentX = waveEnd + var waveSign = if (phaseOffset < waveLength / 2) 1f else -1f + path.rewind() + + // Draw flat line from end to wave endpoint + path.moveTo(bounds.width().toFloat(), 0f) + path.lineTo(waveEnd, 0f) + + // First wave has shortened wavelength + // approx quarter wave gets us to first wave peak + // shouldn't be big enough to notice it's not a sin wave + currentX -= phaseOffset % (waveLength / 2) + val controlRatio = 0.25f + var currentAmp = computeAmplitude(currentX, waveSign) + path.cubicTo( + waveEnd, currentAmp * controlRatio, + lerp(currentX, waveEnd, controlRatio), currentAmp, + currentX, currentAmp) + + // Other waves have full wavelength + val dist = -1 * waveLength / 2f + while (currentX > waveStart) { + waveSign = -waveSign + val nextX = currentX + dist + val midX = currentX + dist / 2 + val nextAmp = computeAmplitude(nextX, waveSign) + path.cubicTo( + midX, currentAmp, + midX, nextAmp, + nextX, nextAmp) + currentAmp = nextAmp + currentX = nextX + } + + // Draw path; clip to progress position canvas.save() canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat()) - // Clip drawing, so we stop at the thumb canvas.clipRect( 0f, -lineAmplitude - strokeWidth, totalProgressPx, lineAmplitude + strokeWidth) - - // The squiggly line - val start = phaseOffset - var currentX = start - var waveSign = 1f - path.rewind() - path.moveTo(start, lineAmplitude * heightFraction) - while (currentX < totalProgressPx) { - val nextX = currentX + waveLength / 2f - val nextWaveSign = waveSign * -1 - path.cubicTo( - currentX + waveLength / 4f, lineAmplitude * waveSign * heightFraction, - nextX - waveLength / 4f, lineAmplitude * nextWaveSign * heightFraction, - nextX, lineAmplitude * nextWaveSign * heightFraction) - currentX = nextX - waveSign = nextWaveSign - } - wavePaint.style = Paint.Style.STROKE canvas.drawPath(path, wavePaint) canvas.restore() + // Draw path; clip between progression position & far edge + canvas.save() + canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat()) + canvas.clipRect( + totalProgressPx, + -lineAmplitude - strokeWidth, + bounds.width().toFloat(), + lineAmplitude + strokeWidth) + canvas.drawPath(path, linePaint) + canvas.restore() + // Draw round line cap at the beginning of the wave - val startAmp = cos(abs(phaseOffset) / waveLength * TWO_PI) - val p = Paint() - p.color = Color.WHITE + val startAmp = cos(abs(waveEnd - phaseOffset) / waveLength * TWO_PI) canvas.drawPoint( bounds.left.toFloat(), bounds.centerY() + startAmp * lineAmplitude * heightFraction, wavePaint) - - // Draw continuous line, to the right of the thumb - canvas.drawLine( - bounds.left.toFloat() + totalProgressPx, - bounds.centerY().toFloat(), - bounds.width().toFloat(), - bounds.centerY().toFloat(), - linePaint) } override fun getOpacity(): Int { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt index e3cd90b13422a..d087b0fe4413a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/SquigglyProgressTest.kt @@ -19,7 +19,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.anyFloat +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit @@ -35,8 +35,7 @@ class SquigglyProgressTest : SysuiTestCase() { lateinit var squigglyProgress: SquigglyProgress @Mock lateinit var canvas: Canvas - @Captor lateinit var wavePaintCaptor: ArgumentCaptor - @Captor lateinit var linePaintCaptor: ArgumentCaptor + @Captor lateinit var paintCaptor: ArgumentCaptor @JvmField @Rule val mockitoRule = MockitoJUnit.rule() @Before @@ -53,9 +52,7 @@ class SquigglyProgressTest : SysuiTestCase() { fun testDrawPathAndLine() { squigglyProgress.draw(canvas) - verify(canvas).drawPath(any(), wavePaintCaptor.capture()) - verify(canvas).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), - linePaintCaptor.capture()) + verify(canvas, times(2)).drawPath(any(), paintCaptor.capture()) } @Test @@ -69,12 +66,11 @@ class SquigglyProgressTest : SysuiTestCase() { fun testStrokeWidth() { squigglyProgress.draw(canvas) - verify(canvas).drawPath(any(), wavePaintCaptor.capture()) - verify(canvas).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), - linePaintCaptor.capture()) + verify(canvas, times(2)).drawPath(any(), paintCaptor.capture()) + val (wavePaint, linePaint) = paintCaptor.getAllValues() - assertThat(wavePaintCaptor.value.strokeWidth).isEqualTo(strokeWidth) - assertThat(linePaintCaptor.value.strokeWidth).isEqualTo(strokeWidth) + assertThat(wavePaint.strokeWidth).isEqualTo(strokeWidth) + assertThat(linePaint.strokeWidth).isEqualTo(strokeWidth) } @Test @@ -82,13 +78,12 @@ class SquigglyProgressTest : SysuiTestCase() { squigglyProgress.alpha = alpha squigglyProgress.draw(canvas) - verify(canvas).drawPath(any(), wavePaintCaptor.capture()) - verify(canvas).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), - linePaintCaptor.capture()) + verify(canvas, times(2)).drawPath(any(), paintCaptor.capture()) + val (wavePaint, linePaint) = paintCaptor.getAllValues() assertThat(squigglyProgress.alpha).isEqualTo(alpha) - assertThat(wavePaintCaptor.value.alpha).isEqualTo(alpha) - assertThat(linePaintCaptor.value.alpha).isEqualTo((alpha / 255f * DISABLED_ALPHA).toInt()) + assertThat(wavePaint.alpha).isEqualTo(alpha) + assertThat(linePaint.alpha).isEqualTo((alpha / 255f * DISABLED_ALPHA).toInt()) } @Test @@ -96,12 +91,11 @@ class SquigglyProgressTest : SysuiTestCase() { squigglyProgress.colorFilter = colorFilter squigglyProgress.draw(canvas) - verify(canvas).drawPath(any(), wavePaintCaptor.capture()) - verify(canvas).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), - linePaintCaptor.capture()) + verify(canvas, times(2)).drawPath(any(), paintCaptor.capture()) + val (wavePaint, linePaint) = paintCaptor.getAllValues() - assertThat(wavePaintCaptor.value.colorFilter).isEqualTo(colorFilter) - assertThat(linePaintCaptor.value.colorFilter).isEqualTo(colorFilter) + assertThat(wavePaint.colorFilter).isEqualTo(colorFilter) + assertThat(linePaint.colorFilter).isEqualTo(colorFilter) } @Test @@ -109,12 +103,11 @@ class SquigglyProgressTest : SysuiTestCase() { squigglyProgress.setTint(tint) squigglyProgress.draw(canvas) - verify(canvas).drawPath(any(), wavePaintCaptor.capture()) - verify(canvas).drawLine(anyFloat(), anyFloat(), anyFloat(), anyFloat(), - linePaintCaptor.capture()) + verify(canvas, times(2)).drawPath(any(), paintCaptor.capture()) + val (wavePaint, linePaint) = paintCaptor.getAllValues() - assertThat(wavePaintCaptor.value.color).isEqualTo(tint) - assertThat(linePaintCaptor.value.color).isEqualTo( + assertThat(wavePaint.color).isEqualTo(tint) + assertThat(linePaint.color).isEqualTo( ColorUtils.setAlphaComponent(tint, DISABLED_ALPHA)) } } \ No newline at end of file