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