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.animation.Animator 19 import android.animation.AnimatorListenerAdapter 20 import android.animation.ObjectAnimator 21 import android.animation.ValueAnimator 22 import android.content.Context 23 import android.os.Trace 24 import android.util.FloatProperty 25 import android.util.Log 26 import android.view.animation.AnimationUtils.loadInterpolator 27 import androidx.dynamicanimation.animation.DynamicAnimation 28 import androidx.dynamicanimation.animation.FloatPropertyCompat 29 import androidx.dynamicanimation.animation.SpringAnimation 30 import androidx.dynamicanimation.animation.SpringForce 31 import com.android.systemui.unfold.UnfoldTransitionProgressProvider 32 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener 33 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED 34 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN 35 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN 36 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING 37 import com.android.systemui.unfold.updates.FoldStateProvider 38 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate 39 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener 40 import com.android.systemui.unfold.updates.name 41 import javax.inject.Inject 42 43 /** Maps fold updates to unfold transition progress using DynamicAnimation. */ 44 class PhysicsBasedUnfoldTransitionProgressProvider 45 @Inject 46 constructor(context: Context, private val foldStateProvider: FoldStateProvider) : 47 UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener { 48 49 private val emphasizedInterpolator = 50 loadInterpolator(context, android.R.interpolator.fast_out_extra_slow_in) 51 52 private var cannedAnimator: ValueAnimator? = null 53 54 private val springAnimation = 55 SpringAnimation( 56 this, 57 FloatPropertyCompat.createFloatPropertyCompat(AnimationProgressProperty) 58 ) 59 .apply { addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider) } 60 61 private var isTransitionRunning = false 62 private var isAnimatedCancelRunning = false 63 64 private var transitionProgress: Float = 0.0f 65 set(value) { 66 if (isTransitionRunning) { 67 listeners.forEach { it.onTransitionProgress(value) } 68 } 69 field = value 70 } 71 72 private val listeners: MutableList<TransitionProgressListener> = mutableListOf() 73 74 init { 75 foldStateProvider.addCallback(this) 76 foldStateProvider.start() 77 } 78 79 override fun destroy() { 80 foldStateProvider.stop() 81 } 82 83 override fun onHingeAngleUpdate(angle: Float) { 84 if (!isTransitionRunning || isAnimatedCancelRunning) return 85 val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION) 86 springAnimation.animateToFinalPosition(progress) 87 } 88 89 private fun saturate(amount: Float, low: Float = 0f, high: Float = 1f): Float = 90 if (amount < low) low else if (amount > high) high else amount 91 92 override fun onFoldUpdate(@FoldUpdate update: Int) { 93 when (update) { 94 FOLD_UPDATE_FINISH_FULL_OPEN, 95 FOLD_UPDATE_FINISH_HALF_OPEN -> { 96 // Do not cancel if we haven't started the transition yet. 97 // This could happen when we fully unfolded the device before the screen 98 // became available. In this case we start and immediately cancel the animation 99 // in onUnfoldedScreenAvailable event handler, so we don't need to cancel it here. 100 if (isTransitionRunning) { 101 cancelTransition(endValue = 1f, animate = true) 102 } 103 } 104 FOLD_UPDATE_FINISH_CLOSED -> { 105 cancelTransition(endValue = 0f, animate = false) 106 } 107 FOLD_UPDATE_START_CLOSING -> { 108 // The transition might be already running as the device might start closing several 109 // times before reaching an end state. 110 if (isTransitionRunning) { 111 // If we are cancelling the animation, reset that so we can resume it normally. 112 // The animation could be 'cancelled' when the user stops folding/unfolding 113 // for some period of time or fully unfolds the device. In this case, 114 // it is forced to run to the end ignoring all further hinge angle events. 115 // By resetting this flag we allow reacting to hinge angle events again, so 116 // the transition continues running. 117 if (isAnimatedCancelRunning) { 118 isAnimatedCancelRunning = false 119 120 // Switching to spring animation, start the animation if it 121 // is not running already 122 springAnimation.animateToFinalPosition(1.0f) 123 124 cannedAnimator?.removeAllListeners() 125 cannedAnimator?.cancel() 126 cannedAnimator = null 127 } 128 } else { 129 startTransition(startValue = 1f) 130 } 131 } 132 } 133 134 if (DEBUG) { 135 Log.d(TAG, "onFoldUpdate = ${update.name()}") 136 Trace.setCounter("fold_update", update.toLong()) 137 } 138 } 139 140 override fun onUnfoldedScreenAvailable() { 141 startTransition(startValue = 0f) 142 143 // Stop the animation if the device has already opened by the time when 144 // the display is available as we won't receive the full open event anymore 145 if (foldStateProvider.isFinishedOpening) { 146 cancelTransition(endValue = 1f, animate = true) 147 } 148 } 149 150 private fun cancelTransition(endValue: Float, animate: Boolean) { 151 if (isTransitionRunning && animate) { 152 if (endValue == 1.0f && !isAnimatedCancelRunning) { 153 listeners.forEach { it.onTransitionFinishing() } 154 } 155 156 isAnimatedCancelRunning = true 157 158 if (USE_CANNED_ANIMATION) { 159 startCannedCancelAnimation() 160 } else { 161 springAnimation.animateToFinalPosition(endValue) 162 } 163 } else { 164 transitionProgress = endValue 165 isAnimatedCancelRunning = false 166 isTransitionRunning = false 167 springAnimation.cancel() 168 169 cannedAnimator?.removeAllListeners() 170 cannedAnimator?.cancel() 171 cannedAnimator = null 172 173 listeners.forEach { it.onTransitionFinished() } 174 175 if (DEBUG) { 176 Log.d(TAG, "onTransitionFinished") 177 } 178 } 179 } 180 181 override fun onAnimationEnd( 182 animation: DynamicAnimation<out DynamicAnimation<*>>, 183 canceled: Boolean, 184 value: Float, 185 velocity: Float 186 ) { 187 if (isAnimatedCancelRunning) { 188 cancelTransition(value, animate = false) 189 } 190 } 191 192 private fun onStartTransition() { 193 Trace.beginSection("$TAG#onStartTransition") 194 listeners.forEach { it.onTransitionStarted() } 195 Trace.endSection() 196 197 isTransitionRunning = true 198 199 if (DEBUG) { 200 Log.d(TAG, "onTransitionStarted") 201 } 202 } 203 204 private fun startTransition(startValue: Float) { 205 if (!isTransitionRunning) onStartTransition() 206 207 springAnimation.apply { 208 spring = 209 SpringForce().apply { 210 finalPosition = startValue 211 dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY 212 stiffness = SPRING_STIFFNESS 213 } 214 minimumVisibleChange = MINIMAL_VISIBLE_CHANGE 215 setStartValue(startValue) 216 setMinValue(0f) 217 setMaxValue(1f) 218 } 219 220 springAnimation.start() 221 } 222 223 override fun addCallback(listener: TransitionProgressListener) { 224 listeners.add(listener) 225 } 226 227 override fun removeCallback(listener: TransitionProgressListener) { 228 listeners.remove(listener) 229 } 230 231 private fun startCannedCancelAnimation() { 232 cannedAnimator?.cancel() 233 cannedAnimator = null 234 235 // Temporary remove listener to cancel the spring animation without 236 // finishing the transition 237 springAnimation.removeEndListener(this) 238 springAnimation.cancel() 239 springAnimation.addEndListener(this) 240 241 cannedAnimator = 242 ObjectAnimator.ofFloat(this, AnimationProgressProperty, transitionProgress, 1f).apply { 243 addListener(CannedAnimationListener()) 244 duration = 245 (CANNED_ANIMATION_DURATION_MS.toFloat() * (1f - transitionProgress)).toLong() 246 interpolator = emphasizedInterpolator 247 start() 248 } 249 } 250 251 private inner class CannedAnimationListener : AnimatorListenerAdapter() { 252 override fun onAnimationStart(animator: Animator) { 253 Trace.beginAsyncSection("$TAG#cannedAnimatorRunning", 0) 254 } 255 256 override fun onAnimationEnd(animator: Animator) { 257 cancelTransition(1f, animate = false) 258 Trace.endAsyncSection("$TAG#cannedAnimatorRunning", 0) 259 } 260 } 261 262 private object AnimationProgressProperty : 263 FloatProperty<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") { 264 265 override fun setValue( 266 provider: PhysicsBasedUnfoldTransitionProgressProvider, 267 value: Float 268 ) { 269 provider.transitionProgress = value 270 } 271 272 override fun get(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float = 273 provider.transitionProgress 274 } 275 } 276 277 private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider" 278 private const val DEBUG = true 279 280 private const val USE_CANNED_ANIMATION = true 281 private const val CANNED_ANIMATION_DURATION_MS = 1000 282 private const val SPRING_STIFFNESS = 600.0f 283 private const val MINIMAL_VISIBLE_CHANGE = 0.001f 284 private const val FINAL_HINGE_ANGLE_POSITION = 165f 285