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