1 /* 2 * Copyright (C) 2021 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.unfold.progress 17 18 import android.util.Log 19 import android.util.MathUtils.saturate 20 import androidx.dynamicanimation.animation.DynamicAnimation 21 import androidx.dynamicanimation.animation.FloatPropertyCompat 22 import androidx.dynamicanimation.animation.SpringAnimation 23 import androidx.dynamicanimation.animation.SpringForce 24 import com.android.systemui.unfold.UnfoldTransitionProgressProvider 25 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener 26 import com.android.systemui.unfold.updates.FOLD_UPDATE_ABORTED 27 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED 28 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING 29 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN 30 import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE 31 import com.android.systemui.unfold.updates.FoldStateProvider 32 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate 33 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener 34 35 /** 36 * Maps fold updates to unfold transition progress using DynamicAnimation. 37 * 38 * TODO(b/193793338) Current limitations: 39 * - doesn't handle postures 40 */ 41 internal class PhysicsBasedUnfoldTransitionProgressProvider( 42 private val foldStateProvider: FoldStateProvider 43 ) : 44 UnfoldTransitionProgressProvider, 45 FoldUpdatesListener, 46 DynamicAnimation.OnAnimationEndListener { 47 48 private val springAnimation = SpringAnimation(this, AnimationProgressProperty) 49 .apply { 50 addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider) 51 } 52 53 private var isTransitionRunning = false 54 private var isAnimatedCancelRunning = false 55 56 private var transitionProgress: Float = 0.0f 57 set(value) { 58 if (isTransitionRunning) { 59 listeners.forEach { it.onTransitionProgress(value) } 60 } 61 field = value 62 } 63 64 private val listeners: MutableList<TransitionProgressListener> = mutableListOf() 65 66 init { 67 foldStateProvider.addCallback(this) 68 foldStateProvider.start() 69 } 70 71 override fun destroy() { 72 foldStateProvider.stop() 73 } 74 75 override fun onHingeAngleUpdate(angle: Float) { 76 if (!isTransitionRunning || isAnimatedCancelRunning) return 77 val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION) 78 springAnimation.animateToFinalPosition(progress) 79 } 80 81 override fun onFoldUpdate(@FoldUpdate update: Int) { 82 when (update) { 83 FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> { 84 startTransition(startValue = 0f) 85 86 // Stop the animation if the device has already opened by the time when 87 // the display is available as we won't receive the full open event anymore 88 if (foldStateProvider.isFullyOpened) { 89 cancelTransition(endValue = 1f, animate = true) 90 } 91 } 92 FOLD_UPDATE_FINISH_FULL_OPEN, FOLD_UPDATE_ABORTED -> { 93 // Do not cancel if we haven't started the transition yet. 94 // This could happen when we fully unfolded the device before the screen 95 // became available. In this case we start and immediately cancel the animation 96 // in FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE event handler, so we don't need to 97 // cancel it here. 98 if (isTransitionRunning) { 99 cancelTransition(endValue = 1f, animate = true) 100 } 101 } 102 FOLD_UPDATE_FINISH_CLOSED -> { 103 cancelTransition(endValue = 0f, animate = false) 104 } 105 FOLD_UPDATE_START_CLOSING -> { 106 // The transition might be already running as the device might start closing several 107 // times before reaching an end state. 108 if (!isTransitionRunning) { 109 startTransition(startValue = 1f) 110 } 111 } 112 } 113 114 if (DEBUG) { 115 Log.d(TAG, "onFoldUpdate = $update") 116 } 117 } 118 119 private fun cancelTransition(endValue: Float, animate: Boolean) { 120 if (isTransitionRunning && animate) { 121 isAnimatedCancelRunning = true 122 springAnimation.animateToFinalPosition(endValue) 123 } else { 124 transitionProgress = endValue 125 isAnimatedCancelRunning = false 126 isTransitionRunning = false 127 springAnimation.cancel() 128 129 listeners.forEach { 130 it.onTransitionFinished() 131 } 132 133 if (DEBUG) { 134 Log.d(TAG, "onTransitionFinished") 135 } 136 } 137 } 138 139 override fun onAnimationEnd( 140 animation: DynamicAnimation<out DynamicAnimation<*>>, 141 canceled: Boolean, 142 value: Float, 143 velocity: Float 144 ) { 145 if (isAnimatedCancelRunning) { 146 cancelTransition(value, animate = false) 147 } 148 } 149 150 private fun onStartTransition() { 151 listeners.forEach { 152 it.onTransitionStarted() 153 } 154 isTransitionRunning = true 155 156 if (DEBUG) { 157 Log.d(TAG, "onTransitionStarted") 158 } 159 } 160 161 private fun startTransition(startValue: Float) { 162 if (!isTransitionRunning) onStartTransition() 163 164 springAnimation.apply { 165 spring = SpringForce().apply { 166 finalPosition = startValue 167 dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY 168 stiffness = SPRING_STIFFNESS 169 } 170 minimumVisibleChange = MINIMAL_VISIBLE_CHANGE 171 setStartValue(startValue) 172 setMinValue(0f) 173 setMaxValue(1f) 174 } 175 176 springAnimation.start() 177 } 178 179 override fun addCallback(listener: TransitionProgressListener) { 180 listeners.add(listener) 181 } 182 183 override fun removeCallback(listener: TransitionProgressListener) { 184 listeners.remove(listener) 185 } 186 187 private object AnimationProgressProperty : 188 FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") { 189 190 override fun setValue( 191 provider: PhysicsBasedUnfoldTransitionProgressProvider, 192 value: Float 193 ) { 194 provider.transitionProgress = value 195 } 196 197 override fun getValue(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float = 198 provider.transitionProgress 199 } 200 } 201 202 private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider" 203 private const val DEBUG = true 204 205 private const val SPRING_STIFFNESS = 200.0f 206 private const val MINIMAL_VISIBLE_CHANGE = 0.001f 207 private const val FINAL_HINGE_ANGLE_POSITION = 165f 208