<lambda>null1 package com.android.systemui.statusbar.notification
2
3 import android.util.FloatProperty
4 import android.view.View
5 import androidx.annotation.FloatRange
6 import com.android.systemui.res.R
7 import com.android.systemui.statusbar.notification.stack.AnimationProperties
8 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
9 import kotlin.math.abs
10
11 /**
12 * Interface that allows to request/retrieve top and bottom roundness (a value between 0f and 1f).
13 *
14 * To request a roundness value, an [SourceType] must be specified. In case more origins require
15 * different roundness, for the same property, the maximum value will always be chosen.
16 *
17 * It also returns the current radius for all corners ([updatedRadii]).
18 */
19 interface Roundable {
20 /** Properties required for a Roundable */
21 val roundableState: RoundableState
22
23 val clipHeight: Int
24
25 /** Current top roundness */
26 @get:FloatRange(from = 0.0, to = 1.0)
27 val topRoundness: Float
28 get() = roundableState.topRoundness
29
30 /** Current bottom roundness */
31 @get:FloatRange(from = 0.0, to = 1.0)
32 val bottomRoundness: Float
33 get() = roundableState.bottomRoundness
34
35 /** Max radius in pixel */
36 val maxRadius: Float
37 get() = roundableState.maxRadius
38
39 /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
40 val topCornerRadius: Float
41 get() = roundableState.topCornerRadius
42
43 /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
44 val bottomCornerRadius: Float
45 get() = roundableState.bottomCornerRadius
46
47 /** Get and update the current radii */
48 val updatedRadii: FloatArray
49 get() =
50 roundableState.radiiBuffer.also { radii ->
51 updateRadii(
52 topCornerRadius = topCornerRadius,
53 bottomCornerRadius = bottomCornerRadius,
54 radii = radii,
55 )
56 }
57
58 /**
59 * Request the top roundness [value] for a specific [sourceType].
60 *
61 * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
62 * origins require different roundness, for the same property, the maximum value will always be
63 * chosen.
64 *
65 * @param value a value between 0f and 1f.
66 * @param animate true if it should animate to that value.
67 * @param sourceType the source from which the request for roundness comes.
68 * @return Whether the roundness was changed.
69 */
70 fun requestTopRoundness(
71 @FloatRange(from = 0.0, to = 1.0) value: Float,
72 sourceType: SourceType,
73 animate: Boolean,
74 ): Boolean {
75 val roundnessMap = roundableState.topRoundnessMap
76 val lastValue = roundnessMap.values.maxOrNull() ?: 0f
77 if (value == 0f) {
78 // we should only take the largest value, and since the smallest value is 0f, we can
79 // remove this value from the list. In the worst case, the list is empty and the
80 // default value is 0f.
81 roundnessMap.remove(sourceType)
82 } else {
83 roundnessMap[sourceType] = value
84 }
85 val newValue = roundnessMap.values.maxOrNull() ?: 0f
86
87 if (lastValue != newValue) {
88 val wasAnimating = roundableState.isTopAnimating()
89
90 // Fail safe:
91 // when we've been animating previously and we're now getting an update in the
92 // other direction, make sure to animate it too, otherwise, the localized updating
93 // may make the start larger than 1.0.
94 val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
95
96 roundableState.setTopRoundness(value = newValue, animated = shouldAnimate || animate)
97 return true
98 }
99 return false
100 }
101
102 /**
103 * Request the top roundness [value] for a specific [sourceType]. Animate the roundness if the
104 * view is shown.
105 *
106 * The top roundness of a [Roundable] can be defined by different [sourceType]. In case more
107 * origins require different roundness, for the same property, the maximum value will always be
108 * chosen.
109 *
110 * @param value a value between 0f and 1f.
111 * @param sourceType the source from which the request for roundness comes.
112 * @return Whether the roundness was changed.
113 */
114 fun requestTopRoundness(
115 @FloatRange(from = 0.0, to = 1.0) value: Float,
116 sourceType: SourceType,
117 ): Boolean {
118 return requestTopRoundness(
119 value = value,
120 sourceType = sourceType,
121 animate = roundableState.targetView.isShown,
122 )
123 }
124
125 /**
126 * Request the bottom roundness [value] for a specific [sourceType].
127 *
128 * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
129 * origins require different roundness, for the same property, the maximum value will always be
130 * chosen.
131 *
132 * @param value value between 0f and 1f.
133 * @param animate true if it should animate to that value.
134 * @param sourceType the source from which the request for roundness comes.
135 * @return Whether the roundness was changed.
136 */
137 fun requestBottomRoundness(
138 @FloatRange(from = 0.0, to = 1.0) value: Float,
139 sourceType: SourceType,
140 animate: Boolean,
141 ): Boolean {
142 val roundnessMap = roundableState.bottomRoundnessMap
143 val lastValue = roundnessMap.values.maxOrNull() ?: 0f
144 if (value == 0f) {
145 // we should only take the largest value, and since the smallest value is 0f, we can
146 // remove this value from the list. In the worst case, the list is empty and the
147 // default value is 0f.
148 roundnessMap.remove(sourceType)
149 } else {
150 roundnessMap[sourceType] = value
151 }
152 val newValue = roundnessMap.values.maxOrNull() ?: 0f
153
154 if (lastValue != newValue) {
155 val wasAnimating = roundableState.isBottomAnimating()
156
157 // Fail safe:
158 // when we've been animating previously and we're now getting an update in the
159 // other direction, make sure to animate it too, otherwise, the localized updating
160 // may make the start larger than 1.0.
161 val shouldAnimate = wasAnimating && abs(newValue - lastValue) > 0.5f
162
163 roundableState.setBottomRoundness(value = newValue, animated = shouldAnimate || animate)
164 return true
165 }
166 return false
167 }
168
169 /**
170 * Request the bottom roundness [value] for a specific [sourceType]. Animate the roundness if
171 * the view is shown.
172 *
173 * The bottom roundness of a [Roundable] can be defined by different [sourceType]. In case more
174 * origins require different roundness, for the same property, the maximum value will always be
175 * chosen.
176 *
177 * @param value value between 0f and 1f.
178 * @param sourceType the source from which the request for roundness comes.
179 * @return Whether the roundness was changed.
180 */
181 fun requestBottomRoundness(
182 @FloatRange(from = 0.0, to = 1.0) value: Float,
183 sourceType: SourceType,
184 ): Boolean {
185 return requestBottomRoundness(
186 value = value,
187 sourceType = sourceType,
188 animate = roundableState.targetView.isShown,
189 )
190 }
191
192 /**
193 * Request the roundness [value] for a specific [sourceType].
194 *
195 * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
196 * more origins require different roundness, for the same property, the maximum value will
197 * always be chosen.
198 *
199 * @param top top value between 0f and 1f.
200 * @param bottom bottom value between 0f and 1f.
201 * @param sourceType the source from which the request for roundness comes.
202 * @param animate true if it should animate to that value.
203 * @return Whether the roundness was changed.
204 */
205 fun requestRoundness(
206 @FloatRange(from = 0.0, to = 1.0) top: Float,
207 @FloatRange(from = 0.0, to = 1.0) bottom: Float,
208 sourceType: SourceType,
209 animate: Boolean,
210 ): Boolean {
211 val hasTopChanged =
212 requestTopRoundness(value = top, sourceType = sourceType, animate = animate)
213 val hasBottomChanged =
214 requestBottomRoundness(value = bottom, sourceType = sourceType, animate = animate)
215 return hasTopChanged || hasBottomChanged
216 }
217
218 /**
219 * Request the roundness [value] for a specific [sourceType]. Animate the roundness if the view
220 * is shown.
221 *
222 * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
223 * more origins require different roundness, for the same property, the maximum value will
224 * always be chosen.
225 *
226 * @param top top value between 0f and 1f.
227 * @param bottom bottom value between 0f and 1f.
228 * @param sourceType the source from which the request for roundness comes.
229 * @return Whether the roundness was changed.
230 */
231 fun requestRoundness(
232 @FloatRange(from = 0.0, to = 1.0) top: Float,
233 @FloatRange(from = 0.0, to = 1.0) bottom: Float,
234 sourceType: SourceType,
235 ): Boolean {
236 return requestRoundness(
237 top = top,
238 bottom = bottom,
239 sourceType = sourceType,
240 animate = roundableState.targetView.isShown,
241 )
242 }
243
244 /**
245 * Request the roundness 0f for a [SourceType].
246 *
247 * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
248 * more origins require different roundness, for the same property, the maximum value will
249 * always be chosen.
250 *
251 * @param sourceType the source from which the request for roundness comes.
252 * @param animate true if it should animate to that value.
253 */
254 fun requestRoundnessReset(sourceType: SourceType, animate: Boolean) {
255 requestRoundness(top = 0f, bottom = 0f, sourceType = sourceType, animate = animate)
256 }
257
258 /**
259 * Request the roundness 0f for a [SourceType]. Animate the roundness if the view is shown.
260 *
261 * The top/bottom roundness of a [Roundable] can be defined by different [sourceType]. In case
262 * more origins require different roundness, for the same property, the maximum value will
263 * always be chosen.
264 *
265 * @param sourceType the source from which the request for roundness comes.
266 */
267 fun requestRoundnessReset(sourceType: SourceType) {
268 requestRoundnessReset(sourceType = sourceType, animate = roundableState.targetView.isShown)
269 }
270
271 /** Apply the roundness changes, usually means invalidate the [RoundableState.targetView]. */
272 fun applyRoundnessAndInvalidate() {
273 roundableState.targetView.invalidate()
274 }
275
276 /** @return true if top or bottom roundness is not zero. */
277 fun hasRoundedCorner(): Boolean {
278 return topRoundness != 0f || bottomRoundness != 0f
279 }
280
281 /**
282 * Update an Array of 8 values, 4 pairs of [X,Y] radii. As expected by param radii of
283 * [android.graphics.Path.addRoundRect].
284 *
285 * This method reuses the previous [radii] for performance reasons.
286 */
287 fun updateRadii(topCornerRadius: Float, bottomCornerRadius: Float, radii: FloatArray) {
288 if (radii.size != 8) error("Unexpected radiiBuffer size ${radii.size}")
289
290 if (radii[0] != topCornerRadius || radii[4] != bottomCornerRadius) {
291 (0..3).forEach { radii[it] = topCornerRadius }
292 (4..7).forEach { radii[it] = bottomCornerRadius }
293 }
294 }
295 }
296
297 /**
298 * State object for a `Roundable` class.
299 *
300 * @param targetView Will handle the [AnimatableProperty]
301 * @param roundable Target of the radius animation
302 * @param maxRadius Max corner radius in pixels
303 */
304 class RoundableState
305 @JvmOverloads
306 constructor(internal val targetView: View, private val roundable: Roundable, maxRadius: Float) {
307 internal var maxRadius = maxRadius
308 private set
309
310 /** Animatable for top roundness */
311 private val topAnimatable = topAnimatable(roundable)
312
313 /** Animatable for bottom roundness */
314 private val bottomAnimatable = bottomAnimatable(roundable)
315
316 /** Current top roundness. Use [setTopRoundness] to update this value */
317 @set:FloatRange(from = 0.0, to = 1.0)
318 internal var topRoundness = 0f
319 private set
320
321 /** Current bottom roundness. Use [setBottomRoundness] to update this value */
322 @set:FloatRange(from = 0.0, to = 1.0)
323 internal var bottomRoundness = 0f
324 private set
325
326 internal val topCornerRadius: Float
327 get() {
328 val height = roundable.clipHeight
329 val topRadius = topRoundness * maxRadius
330 val bottomRadius = bottomRoundness * maxRadius
331
332 if (height == 0) {
333 return 0f
334 } else if (topRadius + bottomRadius > height) {
335 // The sum of top and bottom corner radii should be at max the clipped height
336 val overShoot = topRadius + bottomRadius - height
337 return topRadius - (overShoot * topRoundness / (topRoundness + bottomRoundness))
338 }
339
340 return topRadius
341 }
342
343 internal val bottomCornerRadius: Float
344 get() {
345 val height = roundable.clipHeight
346 val topRadius = topRoundness * maxRadius
347 val bottomRadius = bottomRoundness * maxRadius
348
349 if (height == 0) {
350 return 0f
351 } else if (topRadius + bottomRadius > height) {
352 // The sum of top and bottom corner radii should be at max the clipped height
353 val overShoot = topRadius + bottomRadius - height
354 return bottomRadius -
355 (overShoot * bottomRoundness / (topRoundness + bottomRoundness))
356 }
357
358 return bottomRadius
359 }
360
361 /** Last requested top roundness associated by [SourceType] */
362 internal val topRoundnessMap = mutableMapOf<SourceType, Float>()
363
364 /** Last requested bottom roundness associated by [SourceType] */
365 internal val bottomRoundnessMap = mutableMapOf<SourceType, Float>()
366
367 /** Last cached radii */
368 internal val radiiBuffer = FloatArray(8)
369
370 /** Is top roundness animation in progress? */
isTopAnimatingnull371 internal fun isTopAnimating() = PropertyAnimator.isAnimating(targetView, topAnimatable)
372
373 /** Is bottom roundness animation in progress? */
374 internal fun isBottomAnimating() = PropertyAnimator.isAnimating(targetView, bottomAnimatable)
375
376 /** Set the current top roundness */
377 internal fun setTopRoundness(value: Float, animated: Boolean) {
378 PropertyAnimator.setProperty(targetView, topAnimatable, value, DURATION, animated)
379 }
380
381 /** Set the current bottom roundness */
setBottomRoundnessnull382 internal fun setBottomRoundness(value: Float, animated: Boolean) {
383 PropertyAnimator.setProperty(targetView, bottomAnimatable, value, DURATION, animated)
384 }
385
setMaxRadiusnull386 fun setMaxRadius(radius: Float) {
387 if (maxRadius != radius) {
388 maxRadius = radius
389 roundable.applyRoundnessAndInvalidate()
390 }
391 }
392
<lambda>null393 fun debugString() = buildString {
394 append("Roundable { ")
395 append("top: { value: $topRoundness, requests: $topRoundnessMap}")
396 append(", ")
397 append("bottom: { value: $bottomRoundness, requests: $bottomRoundnessMap}")
398 append("}")
399 }
400
401 companion object {
402 private val DURATION: AnimationProperties =
403 AnimationProperties()
404 .setDuration(StackStateAnimator.ANIMATION_DURATION_CORNER_RADIUS.toLong())
405
topAnimatablenull406 private fun topAnimatable(roundable: Roundable): AnimatableProperty =
407 AnimatableProperty.from(
408 object : FloatProperty<View>("topRoundness") {
409 override fun get(view: View): Float = roundable.topRoundness
410
411 override fun setValue(view: View, value: Float) {
412 roundable.roundableState.topRoundness = value
413 roundable.applyRoundnessAndInvalidate()
414 }
415 },
416 R.id.top_roundess_animator_tag,
417 R.id.top_roundess_animator_end_tag,
418 R.id.top_roundess_animator_start_tag,
419 )
420
bottomAnimatablenull421 private fun bottomAnimatable(roundable: Roundable): AnimatableProperty =
422 AnimatableProperty.from(
423 object : FloatProperty<View>("bottomRoundness") {
424 override fun get(view: View): Float = roundable.bottomRoundness
425
426 override fun setValue(view: View, value: Float) {
427 roundable.roundableState.bottomRoundness = value
428 roundable.applyRoundnessAndInvalidate()
429 }
430 },
431 R.id.bottom_roundess_animator_tag,
432 R.id.bottom_roundess_animator_end_tag,
433 R.id.bottom_roundess_animator_start_tag,
434 )
435 }
436 }
437
438 /**
439 * Interface used to define the owner of a roundness. Usually the [SourceType] is defined as a
440 * private property of a class.
441 */
442 interface SourceType {
443 companion object {
444 /**
445 * This is the most convenient way to define a new [SourceType].
446 *
447 * For example:
448 * ```kotlin
449 * private val SECTION = SourceType.from("Section")
450 * ```
451 */
452 @JvmStatic
fromnull453 fun from(name: String) =
454 object : SourceType {
455 override fun toString() = name
456 }
457 }
458 }
459