blob: 88f6f3dd9d0e12ce38bab2a05b05c7721429d2e4 [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
}
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 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())
canvas.clipRect(
0f,
-lineAmplitude - strokeWidth,
totalProgressPx,
lineAmplitude + strokeWidth)
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(waveEnd - phaseOffset) / waveLength * TWO_PI)
canvas.drawPoint(
bounds.left.toFloat(),
bounds.centerY() + startAmp * lineAmplitude * heightFraction,
wavePaint)
}
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())
}
}