blob: 6bc94cd5f52576de65dd99c146ad5c3a51696381 [file] [log] [blame]
package com.android.systemui.media
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.res.ColorStateList
import android.graphics.Canvas
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
import kotlin.math.abs
import kotlin.math.cos
private const val TAG = "Squiggly"
private const val TWO_PI = (Math.PI * 2f).toFloat()
@VisibleForTesting
internal const val DISABLED_ALPHA = 77
class SquigglyProgress : Drawable() {
private val wavePaint = Paint()
private val linePaint = Paint()
private val path = Path()
private var heightFraction = 0f
private var heightAnimator: ValueAnimator? = null
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
var lineAmplitude = 0f
// Line speed in px per second
var phaseSpeed = 0f
// Progress stroke width, both for wave and solid line
var strokeWidth = 0f
set(value) {
if (field == value) {
return
}
field = value
wavePaint.strokeWidth = value
linePaint.strokeWidth = value
}
// Enables a transition region where the amplitude
// of the wave is reduced linearly across it.
var transitionEnabled = true
set(value) {
field = value
invalidateSelf()
}
init {
wavePaint.strokeCap = Paint.Cap.ROUND
linePaint.strokeCap = Paint.Cap.ROUND
linePaint.style = Paint.Style.STROKE
wavePaint.style = Paint.Style.STROKE
linePaint.alpha = DISABLED_ALPHA
}
var animate: Boolean = false
set(value) {
if (field == value) {
return
}
field = value
if (field) {
lastFrameTime = SystemClock.uptimeMillis()
}
heightAnimator?.cancel()
heightAnimator = ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply {
if (animate) {
startDelay = 60
duration = 800
interpolator = Interpolators.EMPHASIZED_DECELERATE
} else {
duration = 550
interpolator = Interpolators.STANDARD_DECELERATE
}
addUpdateListener {
heightFraction = it.animatedValue as Float
invalidateSelf()
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
heightAnimator = null
}
})
start()
}
}
override fun draw(canvas: Canvas) {
if (animate) {
invalidateSelf()
val now = SystemClock.uptimeMillis()
phaseOffset += (now - lastFrameTime) / 1000f * phaseSpeed
phaseOffset %= waveLength
lastFrameTime = now
}
val progress = level / 10_000f
val totalWidth = bounds.width().toFloat()
val totalProgressPx = totalWidth * progress
val waveProgressPx = totalWidth * (
if (!transitionEnabled || progress > matchedWaveEndpoint) progress else
lerp(minWaveEndpoint, matchedWaveEndpoint, lerpInv(0f, matchedWaveEndpoint, progress)))
// Build Wiggly Path
val waveStart = -phaseOffset - waveLength / 2f
val waveEnd = if (transitionEnabled) totalWidth else waveProgressPx
// helper function, computes amplitude for wave segment
val computeAmplitude: (Float, Float) -> Float = { x, sign ->
if (transitionEnabled) {
val length = transitionPeriods * waveLength
val coeff = lerpInvSat(
waveProgressPx + length / 2f,
waveProgressPx - length / 2f,
x)
sign * heightFraction * lineAmplitude * coeff
} else {
sign * heightFraction * lineAmplitude
}
}
// Reset path object to the start
path.rewind()
path.moveTo(waveStart, 0f)
// Build the wave, incrementing by half the wavelength each time
var currentX = waveStart
var waveSign = 1f
var currentAmp = computeAmplitude(currentX, waveSign)
val dist = waveLength / 2f
while (currentX < waveEnd) {
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
}
// translate to the start position of the progress bar for all draw commands
val clipTop = lineAmplitude + strokeWidth
canvas.save()
canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat())
// Draw path up to progress position
canvas.save()
canvas.clipRect(0f, -1f * clipTop, totalProgressPx, clipTop)
canvas.drawPath(path, wavePaint)
canvas.restore()
if (transitionEnabled) {
// If there's a smooth transition, we draw the rest of the
// path in a different color (using different clip params)
canvas.save()
canvas.clipRect(totalProgressPx, -1f * clipTop, totalWidth, clipTop)
canvas.drawPath(path, linePaint)
canvas.restore()
} else {
// No transition, just draw a flat line to the end of the region.
// The discontinuity is hidden by the progress bar thumb shape.
canvas.drawLine(totalProgressPx, 0f, totalWidth, 0f, linePaint)
}
// Draw round line cap at the beginning of the wave
val startAmp = cos(abs(waveStart) / waveLength * TWO_PI)
canvas.drawPoint(0f, startAmp * lineAmplitude * heightFraction, wavePaint)
canvas.restore()
}
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
override fun setColorFilter(colorFilter: ColorFilter?) {
wavePaint.colorFilter = colorFilter
linePaint.colorFilter = colorFilter
}
override fun setAlpha(alpha: Int) {
updateColors(wavePaint.color, alpha)
}
override fun getAlpha(): Int {
return wavePaint.alpha
}
override fun setTint(tintColor: Int) {
updateColors(tintColor, alpha)
}
override fun onLevelChange(level: Int): Boolean {
return animate
}
override fun setTintList(tint: ColorStateList?) {
if (tint == null) {
return
}
updateColors(tint.defaultColor, alpha)
}
private fun updateColors(tintColor: Int, alpha: Int) {
wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha)
linePaint.color = ColorUtils.setAlphaComponent(tintColor,
(DISABLED_ALPHA * (alpha / 255f)).toInt())
}
}