• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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