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