1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.media.controls.ui.drawable 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.content.res.ColorStateList 23 import android.graphics.Canvas 24 import android.graphics.ColorFilter 25 import android.graphics.Paint 26 import android.graphics.Path 27 import android.graphics.PixelFormat 28 import android.graphics.drawable.Drawable 29 import android.os.SystemClock 30 import android.util.MathUtils.lerp 31 import android.util.MathUtils.lerpInv 32 import android.util.MathUtils.lerpInvSat 33 import androidx.annotation.VisibleForTesting 34 import com.android.app.animation.Interpolators 35 import com.android.app.tracing.traceSection 36 import com.android.internal.graphics.ColorUtils 37 import kotlin.math.abs 38 import kotlin.math.cos 39 40 private const val TAG = "Squiggly" 41 42 private const val TWO_PI = (Math.PI * 2f).toFloat() 43 @VisibleForTesting internal const val DISABLED_ALPHA = 77 44 45 class SquigglyProgress : Drawable() { 46 47 private val wavePaint = Paint() 48 private val linePaint = Paint() 49 private val path = Path() 50 private var heightFraction = 0f 51 private var heightAnimator: ValueAnimator? = null 52 private var phaseOffset = 0f 53 private var lastFrameTime = -1L 54 55 /* distance over which amplitude drops to zero, measured in wavelengths */ 56 private val transitionPeriods = 1.5f 57 /* wave endpoint as percentage of bar when play position is zero */ 58 private val minWaveEndpoint = 0.2f 59 /* wave endpoint as percentage of bar when play position matches wave endpoint */ 60 private val matchedWaveEndpoint = 0.6f 61 62 // Horizontal length of the sine wave 63 var waveLength = 0f 64 // Height of each peak of the sine wave 65 var lineAmplitude = 0f 66 // Line speed in px per second 67 var phaseSpeed = 0f 68 // Progress stroke width, both for wave and solid line 69 var strokeWidth = 0f 70 set(value) { 71 if (field == value) { 72 return 73 } 74 field = value 75 wavePaint.strokeWidth = value 76 linePaint.strokeWidth = value 77 } 78 79 // Enables a transition region where the amplitude 80 // of the wave is reduced linearly across it. 81 var transitionEnabled = true 82 set(value) { 83 field = value 84 invalidateSelf() 85 } 86 87 init { 88 wavePaint.strokeCap = Paint.Cap.ROUND 89 linePaint.strokeCap = Paint.Cap.ROUND 90 linePaint.style = Paint.Style.STROKE 91 wavePaint.style = Paint.Style.STROKE 92 linePaint.alpha = DISABLED_ALPHA 93 } 94 95 var animate: Boolean = false 96 set(value) { 97 if (field == value) { 98 return 99 } 100 field = value 101 if (field) { 102 lastFrameTime = SystemClock.uptimeMillis() 103 } 104 heightAnimator?.cancel() 105 heightAnimator = 106 ValueAnimator.ofFloat(heightFraction, if (animate) 1f else 0f).apply { 107 if (animate) { 108 startDelay = 60 109 duration = 800 110 interpolator = Interpolators.EMPHASIZED_DECELERATE 111 } else { 112 duration = 550 113 interpolator = Interpolators.STANDARD_DECELERATE 114 } 115 addUpdateListener { 116 heightFraction = it.animatedValue as Float 117 invalidateSelf() 118 } 119 addListener( 120 object : AnimatorListenerAdapter() { 121 override fun onAnimationEnd(animation: Animator) { 122 heightAnimator = null 123 } 124 } 125 ) 126 start() 127 } 128 } 129 130 override fun draw(canvas: Canvas) { 131 traceSection("SquigglyProgress#draw") { drawTraced(canvas) } 132 } 133 134 private fun drawTraced(canvas: Canvas) { 135 if (animate) { 136 invalidateSelf() 137 val now = SystemClock.uptimeMillis() 138 phaseOffset += (now - lastFrameTime) / 1000f * phaseSpeed 139 phaseOffset %= waveLength 140 lastFrameTime = now 141 } 142 143 val progress = level / 10_000f 144 val totalWidth = bounds.width().toFloat() 145 val totalProgressPx = totalWidth * progress 146 val waveProgressPx = 147 totalWidth * 148 (if (!transitionEnabled || progress > matchedWaveEndpoint) progress 149 else 150 lerp( 151 minWaveEndpoint, 152 matchedWaveEndpoint, 153 lerpInv(0f, matchedWaveEndpoint, progress) 154 )) 155 156 // Build Wiggly Path 157 val waveStart = -phaseOffset - waveLength / 2f 158 val waveEnd = if (transitionEnabled) totalWidth else waveProgressPx 159 160 // helper function, computes amplitude for wave segment 161 val computeAmplitude: (Float, Float) -> Float = { x, sign -> 162 if (transitionEnabled) { 163 val length = transitionPeriods * waveLength 164 val coeff = 165 lerpInvSat(waveProgressPx + length / 2f, waveProgressPx - length / 2f, x) 166 sign * heightFraction * lineAmplitude * coeff 167 } else { 168 sign * heightFraction * lineAmplitude 169 } 170 } 171 172 // Reset path object to the start 173 path.rewind() 174 path.moveTo(waveStart, 0f) 175 176 // Build the wave, incrementing by half the wavelength each time 177 var currentX = waveStart 178 var waveSign = 1f 179 var currentAmp = computeAmplitude(currentX, waveSign) 180 val dist = waveLength / 2f 181 while (currentX < waveEnd) { 182 waveSign = -waveSign 183 val nextX = currentX + dist 184 val midX = currentX + dist / 2 185 val nextAmp = computeAmplitude(nextX, waveSign) 186 path.cubicTo(midX, currentAmp, midX, nextAmp, nextX, nextAmp) 187 currentAmp = nextAmp 188 currentX = nextX 189 } 190 191 // translate to the start position of the progress bar for all draw commands 192 val clipTop = lineAmplitude + strokeWidth 193 canvas.save() 194 canvas.translate(bounds.left.toFloat(), bounds.centerY().toFloat()) 195 196 // Draw path up to progress position 197 canvas.save() 198 canvas.clipRect(0f, -1f * clipTop, totalProgressPx, clipTop) 199 canvas.drawPath(path, wavePaint) 200 canvas.restore() 201 202 if (transitionEnabled) { 203 // If there's a smooth transition, we draw the rest of the 204 // path in a different color (using different clip params) 205 canvas.save() 206 canvas.clipRect(totalProgressPx, -1f * clipTop, totalWidth, clipTop) 207 canvas.drawPath(path, linePaint) 208 canvas.restore() 209 } else { 210 // No transition, just draw a flat line to the end of the region. 211 // The discontinuity is hidden by the progress bar thumb shape. 212 canvas.drawLine(totalProgressPx, 0f, totalWidth, 0f, linePaint) 213 } 214 215 // Draw round line cap at the beginning of the wave 216 val startAmp = cos(abs(waveStart) / waveLength * TWO_PI) 217 canvas.drawPoint(0f, startAmp * lineAmplitude * heightFraction, wavePaint) 218 219 canvas.restore() 220 } 221 222 override fun getOpacity(): Int { 223 return PixelFormat.TRANSLUCENT 224 } 225 226 override fun setColorFilter(colorFilter: ColorFilter?) { 227 wavePaint.colorFilter = colorFilter 228 linePaint.colorFilter = colorFilter 229 } 230 231 override fun setAlpha(alpha: Int) { 232 updateColors(wavePaint.color, alpha) 233 } 234 235 override fun getAlpha(): Int { 236 return wavePaint.alpha 237 } 238 239 override fun setTint(tintColor: Int) { 240 updateColors(tintColor, alpha) 241 } 242 243 override fun onLevelChange(level: Int): Boolean { 244 return animate 245 } 246 247 override fun setTintList(tint: ColorStateList?) { 248 if (tint == null) { 249 return 250 } 251 updateColors(tint.defaultColor, alpha) 252 } 253 254 private fun updateColors(tintColor: Int, alpha: Int) { 255 wavePaint.color = ColorUtils.setAlphaComponent(tintColor, alpha) 256 linePaint.color = 257 ColorUtils.setAlphaComponent(tintColor, (DISABLED_ALPHA * (alpha / 255f)).toInt()) 258 } 259 } 260