1 /*
2  * 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.keyguard.ui
17 
18 import android.view.animation.Interpolator
19 import com.android.app.animation.Interpolators.LINEAR
20 import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED
21 import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED
22 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
23 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
24 import com.android.systemui.keyguard.shared.model.TransitionStep
25 import kotlin.math.max
26 import kotlin.math.min
27 import kotlin.time.Duration
28 import kotlin.time.Duration.Companion.milliseconds
29 import kotlinx.coroutines.flow.Flow
30 import kotlinx.coroutines.flow.filterNotNull
31 import kotlinx.coroutines.flow.map
32 
33 /**
34  * For the given transition params, construct a flow using [createFlow] for the specified portion of
35  * the overall transition.
36  */
37 class KeyguardTransitionAnimationFlow(
38     private val transitionDuration: Duration,
39     private val transitionFlow: Flow<TransitionStep>,
40 ) {
41     /**
42      * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted in
43      * the range of [0, 1]. View animations should begin and end within a subset of this range. This
44      * function maps the [startTime] and [duration] into [0, 1], when this subset is valid.
45      */
46     fun createFlow(
47         duration: Duration,
48         onStep: (Float) -> Float,
49         startTime: Duration = 0.milliseconds,
50         onStart: (() -> Unit)? = null,
51         onCancel: (() -> Float)? = null,
52         onFinish: (() -> Float)? = null,
53         interpolator: Interpolator = LINEAR,
54     ): Flow<Float> {
55         if (!duration.isPositive()) {
56             throw IllegalArgumentException("duration must be a positive number: $duration")
57         }
58         if ((startTime + duration).compareTo(transitionDuration) > 0) {
59             throw IllegalArgumentException(
60                 "startTime($startTime) + duration($duration) must be" +
61                     " <= transitionDuration($transitionDuration)"
62             )
63         }
64 
65         val start = (startTime / transitionDuration).toFloat()
66         val chunks = (transitionDuration / duration).toFloat()
67         var isComplete = true
68 
69         fun stepToValue(step: TransitionStep): Float? {
70             val value = (step.value - start) * chunks
71             return when (step.transitionState) {
72                 // When starting, make sure to always emit. If a transition is started from the
73                 // middle, it is possible this animation is being skipped but we need to inform
74                 // the ViewModels of the last update
75                 STARTED -> {
76                     isComplete = false
77                     onStart?.invoke()
78                     max(0f, min(1f, value))
79                 }
80                 // Always send a final value of 1. Because of rounding, [value] may never be
81                 // exactly 1.
82                 RUNNING ->
83                     if (isComplete) {
84                         null
85                     } else if (value >= 1f) {
86                         isComplete = true
87                         1f
88                     } else if (value >= 0f) {
89                         value
90                     } else {
91                         null
92                     }
93                 else -> null
94             }?.let { onStep(interpolator.getInterpolation(it)) }
95         }
96 
97         return transitionFlow
98             .map { step ->
99                 when (step.transitionState) {
100                     STARTED -> stepToValue(step)
101                     RUNNING -> stepToValue(step)
102                     CANCELED -> onCancel?.invoke()
103                     FINISHED -> onFinish?.invoke()
104                 }
105             }
106             .filterNotNull()
107     }
108 }
109