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