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