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