1 /*
<lambda>null2  * Copyright (C) 2023 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 @file:OptIn(ExperimentalCoroutinesApi::class)
17 
18 package com.android.systemui.util.ui
19 
20 import com.android.systemui.util.ui.AnimatedValue.Animating
21 import com.android.systemui.util.ui.AnimatedValue.NotAnimating
22 import kotlinx.coroutines.CompletableDeferred
23 import kotlinx.coroutines.ExperimentalCoroutinesApi
24 import kotlinx.coroutines.flow.Flow
25 import kotlinx.coroutines.flow.transformLatest
26 
27 /**
28  * A state comprised of a [value] of type [T] paired with a boolean indicating whether or not the
29  * [value] [isAnimating] in the UI.
30  */
31 sealed interface AnimatedValue<out T> {
32 
33     /** A [state][value] that is not actively animating in the UI. */
34     data class NotAnimating<out T>(val value: T) : AnimatedValue<T>
35 
36     /**
37      * A [state][value] that is actively animating in the UI. Invoking [onStopAnimating] will signal
38      * the source of the state to stop animating.
39      */
40     data class Animating<out T>(
41         val value: T,
42         val onStopAnimating: () -> Unit,
43     ) : AnimatedValue<T>
44 }
45 
46 /** The state held in this [AnimatedValue]. */
47 inline val <T> AnimatedValue<T>.value: T
48     get() =
49         when (this) {
50             is Animating -> value
51             is NotAnimating -> value
52         }
53 
54 /** Returns whether or not this [AnimatedValue] is animating or not. */
55 inline val <T> AnimatedValue<T>.isAnimating: Boolean
56     get() = this is Animating<T>
57 
58 /**
59  * If this [AnimatedValue] [isAnimating], then signal that the animation should be stopped.
60  * Otherwise, do nothing.
61  */
62 @Suppress("NOTHING_TO_INLINE")
stopAnimatingnull63 inline fun AnimatedValue<*>.stopAnimating() {
64     if (this is Animating) onStopAnimating()
65 }
66 
67 /**
68  * An event comprised of a [value] of type [T] paired with a [boolean][startAnimating] indicating
69  * whether or not this event should start an animation.
70  */
71 data class AnimatableEvent<out T>(
72     val value: T,
73     val startAnimating: Boolean,
74 )
75 
76 /**
77  * Returns a [Flow] that tracks an [AnimatedValue] state. The input [Flow] is used to update the
78  * [AnimatedValue.value], as well as [AnimatedValue.isAnimating] if the event's
79  * [AnimatableEvent.startAnimating] value is `true`. When [completionEvents] emits a value, the
80  * [AnimatedValue.isAnimating] will flip to `false`.
81  */
toAnimatedValueFlownull82 fun <T> Flow<AnimatableEvent<T>>.toAnimatedValueFlow(): Flow<AnimatedValue<T>> =
83     transformLatest { (value, startAnimating) ->
84         if (startAnimating) {
85             val onCompleted = CompletableDeferred<Unit>()
86             emit(Animating(value) { onCompleted.complete(Unit) })
87             // Wait for a completion now that we've started animating
88             onCompleted.await()
89         }
90         emit(NotAnimating(value))
91     }
92 
93 /**
94  * Zip two [AnimatedValue]s together into a single [AnimatedValue], using [block] to combine the
95  * [value]s of each.
96  *
97  * If either [AnimatedValue] [isAnimating], then the result is also animating. Invoking
98  * [stopAnimating] on the result is equivalent to invoking [stopAnimating] on each input.
99  */
zipnull100 inline fun <A, B, Z> zip(
101     valueA: AnimatedValue<A>,
102     valueB: AnimatedValue<B>,
103     block: (A, B) -> Z,
104 ): AnimatedValue<Z> {
105     val zippedValue = block(valueA.value, valueB.value)
106     return when (valueA) {
107         is Animating ->
108             when (valueB) {
109                 is Animating ->
110                     Animating(zippedValue) {
111                         valueA.onStopAnimating()
112                         valueB.onStopAnimating()
113                     }
114                 is NotAnimating -> Animating(zippedValue, valueA.onStopAnimating)
115             }
116         is NotAnimating ->
117             when (valueB) {
118                 is Animating -> Animating(zippedValue, valueB.onStopAnimating)
119                 is NotAnimating -> NotAnimating(zippedValue)
120             }
121     }
122 }
123 
124 /**
125  * Flattens a nested [AnimatedValue], the result of which holds the [value] of the inner
126  * [AnimatedValue].
127  *
128  * If either the outer or inner [AnimatedValue] [isAnimating], then the flattened result is also
129  * animating. Invoking [stopAnimating] on the result is equivalent to invoking [stopAnimating] on
130  * both the outer and inner values.
131  */
132 @Suppress("NOTHING_TO_INLINE")
flattennull133 inline fun <T> AnimatedValue<AnimatedValue<T>>.flatten(): AnimatedValue<T> = flatMap { it }
134 
135 /**
136  * Returns an [AnimatedValue], the [value] of which is the result of the given [value] applied to
137  * [block].
138  */
mapnull139 inline fun <A, B> AnimatedValue<A>.map(block: (A) -> B): AnimatedValue<B> =
140     when (this) {
141         is Animating -> Animating(block(value), ::stopAnimating)
142         is NotAnimating -> NotAnimating(block(value))
143     }
144 
145 /**
146  * Returns an [AnimatedValue] from the result of [block] being invoked on the [value] original
147  * [AnimatedValue].
148  *
149  * If either the input [AnimatedValue] or the result of [block] [isAnimating], then the flattened
150  * result is also animating. Invoking [stopAnimating] on the result is equivalent to invoking
151  * [stopAnimating] on both values.
152  */
flatMapnull153 inline fun <A, B> AnimatedValue<A>.flatMap(block: (A) -> AnimatedValue<B>): AnimatedValue<B> =
154     when (this) {
155         is NotAnimating -> block(value)
156         is Animating ->
157             when (val inner = block(value)) {
158                 is Animating ->
159                     Animating(inner.value) {
160                         onStopAnimating()
161                         inner.onStopAnimating()
162                     }
163                 is NotAnimating -> Animating(inner.value, onStopAnimating)
164             }
165     }
166