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.Handler 24 import android.os.Trace 25 import android.util.FloatProperty 26 import android.util.Log 27 import android.view.animation.AnimationUtils.loadInterpolator 28 import androidx.dynamicanimation.animation.DynamicAnimation 29 import androidx.dynamicanimation.animation.FloatPropertyCompat 30 import androidx.dynamicanimation.animation.SpringAnimation 31 import androidx.dynamicanimation.animation.SpringForce 32 import com.android.systemui.unfold.UnfoldTransitionProgressProvider 33 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener 34 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED 35 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN 36 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN 37 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING 38 import com.android.systemui.unfold.updates.FoldStateProvider 39 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate 40 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener 41 import com.android.systemui.unfold.updates.name 42 import dagger.assisted.Assisted 43 import dagger.assisted.AssistedFactory 44 import dagger.assisted.AssistedInject 45 46 /** 47 * Maps fold updates to unfold transition progress using DynamicAnimation. 48 * 49 * Note that all variable accesses must be done in the [Handler] provided in the constructor, that 50 * might be different than [mainHandler]. When a custom handler is provided, the [SpringAnimation] 51 * uses a scheduler different than the default one. 52 */ 53 class PhysicsBasedUnfoldTransitionProgressProvider 54 @AssistedInject 55 constructor( 56 context: Context, 57 private val schedulerFactory: UnfoldFrameCallbackScheduler.Factory, 58 @Assisted private val foldStateProvider: FoldStateProvider, 59 @Assisted private val progressHandler: Handler, 60 ) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener { 61 62 private val emphasizedInterpolator = 63 loadInterpolator(context, android.R.interpolator.fast_out_extra_slow_in) 64 65 private var cannedAnimator: ValueAnimator? = null 66 67 private val springAnimation = 68 SpringAnimation( 69 this, 70 FloatPropertyCompat.createFloatPropertyCompat(AnimationProgressProperty) 71 ) <lambda>null72 .apply { addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider) } 73 74 private var isTransitionRunning = false 75 private var isAnimatedCancelRunning = false 76 77 private var transitionProgress: Float = 0.0f 78 set(value) { 79 assertInProgressThread() 80 if (isTransitionRunning) { <lambda>null81 listeners.forEach { it.onTransitionProgress(value) } 82 } 83 field = value 84 } 85 86 private val listeners: MutableList<TransitionProgressListener> = mutableListOf() 87 88 init { <lambda>null89 progressHandler.post { 90 // The scheduler needs to be created in the progress handler in order to get the correct 91 // choreographer and frame callbacks. This is because the choreographer can be get only 92 // as a thread local. 93 springAnimation.scheduler = schedulerFactory.create() 94 foldStateProvider.addCallback(this) 95 foldStateProvider.start() 96 } 97 } 98 destroynull99 override fun destroy() { 100 foldStateProvider.stop() 101 } 102 onHingeAngleUpdatenull103 override fun onHingeAngleUpdate(angle: Float) { 104 assertInProgressThread() 105 106 if (!isTransitionRunning || isAnimatedCancelRunning) return 107 val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION) 108 springAnimation.animateToFinalPosition(progress) 109 } 110 saturatenull111 private fun saturate(amount: Float, low: Float = 0f, high: Float = 1f): Float = 112 if (amount < low) low else if (amount > high) high else amount 113 114 override fun onFoldUpdate(@FoldUpdate update: Int) { 115 assertInProgressThread() 116 when (update) { 117 FOLD_UPDATE_FINISH_FULL_OPEN, 118 FOLD_UPDATE_FINISH_HALF_OPEN -> { 119 // Do not cancel if we haven't started the transition yet. 120 // This could happen when we fully unfolded the device before the screen 121 // became available. In this case we start and immediately cancel the animation 122 // in onUnfoldedScreenAvailable event handler, so we don't need to cancel it here. 123 if (isTransitionRunning) { 124 cancelTransition(endValue = 1f, animate = true) 125 } 126 } 127 FOLD_UPDATE_FINISH_CLOSED -> { 128 cancelTransition(endValue = 0f, animate = false) 129 } 130 FOLD_UPDATE_START_CLOSING -> { 131 // The transition might be already running as the device might start closing several 132 // times before reaching an end state. 133 if (isTransitionRunning) { 134 // If we are cancelling the animation, reset that so we can resume it normally. 135 // The animation could be 'cancelled' when the user stops folding/unfolding 136 // for some period of time or fully unfolds the device. In this case, 137 // it is forced to run to the end ignoring all further hinge angle events. 138 // By resetting this flag we allow reacting to hinge angle events again, so 139 // the transition continues running. 140 if (isAnimatedCancelRunning) { 141 isAnimatedCancelRunning = false 142 143 // Switching to spring animation, start the animation if it 144 // is not running already 145 springAnimation.animateToFinalPosition(1.0f) 146 147 cannedAnimator?.removeAllListeners() 148 cannedAnimator?.cancel() 149 cannedAnimator = null 150 } 151 } else { 152 startTransition(startValue = 1f) 153 } 154 } 155 } 156 157 if (DEBUG) { 158 Log.d(TAG, "onFoldUpdate = ${update.name()}") 159 Trace.setCounter("fold_update", update.toLong()) 160 } 161 } 162 onUnfoldedScreenAvailablenull163 override fun onUnfoldedScreenAvailable() { 164 startTransition(startValue = 0f) 165 166 // Stop the animation if the device has already opened by the time when 167 // the display is available as we won't receive the full open event anymore 168 if (foldStateProvider.isFinishedOpening) { 169 cancelTransition(endValue = 1f, animate = true) 170 } 171 } 172 cancelTransitionnull173 private fun cancelTransition(endValue: Float, animate: Boolean) { 174 assertInProgressThread() 175 if (isTransitionRunning && animate) { 176 if (endValue == 1.0f && !isAnimatedCancelRunning) { 177 listeners.forEach { it.onTransitionFinishing() } 178 } 179 180 isAnimatedCancelRunning = true 181 182 if (USE_CANNED_ANIMATION) { 183 startCannedCancelAnimation() 184 } else { 185 springAnimation.animateToFinalPosition(endValue) 186 } 187 } else { 188 transitionProgress = endValue 189 isAnimatedCancelRunning = false 190 isTransitionRunning = false 191 springAnimation.cancel() 192 cannedAnimator?.removeAllListeners() 193 cannedAnimator?.cancel() 194 cannedAnimator = null 195 196 listeners.forEach { it.onTransitionFinished() } 197 198 if (DEBUG) { 199 Log.d(TAG, "onTransitionFinished") 200 } 201 } 202 } 203 onAnimationEndnull204 override fun onAnimationEnd( 205 animation: DynamicAnimation<out DynamicAnimation<*>>, 206 canceled: Boolean, 207 value: Float, 208 velocity: Float, 209 ) { 210 if (isAnimatedCancelRunning) { 211 cancelTransition(value, animate = false) 212 } 213 } 214 onStartTransitionnull215 private fun onStartTransition() { 216 Trace.beginSection("$TAG#onStartTransition") 217 listeners.forEach { it.onTransitionStarted() } 218 Trace.endSection() 219 220 isTransitionRunning = true 221 222 if (DEBUG) { 223 Log.d(TAG, "onTransitionStarted") 224 } 225 } 226 startTransitionnull227 private fun startTransition(startValue: Float) { 228 assertInProgressThread() 229 if (!isTransitionRunning) onStartTransition() 230 231 springAnimation.apply { 232 spring = 233 SpringForce().apply { 234 finalPosition = startValue 235 dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY 236 stiffness = SPRING_STIFFNESS 237 } 238 minimumVisibleChange = MINIMAL_VISIBLE_CHANGE 239 setStartValue(startValue) 240 setMinValue(0f) 241 setMaxValue(1f) 242 } 243 244 springAnimation.start() 245 } 246 addCallbacknull247 override fun addCallback(listener: TransitionProgressListener) { 248 progressHandler.post { listeners.add(listener) } 249 } 250 removeCallbacknull251 override fun removeCallback(listener: TransitionProgressListener) { 252 progressHandler.post { listeners.remove(listener) } 253 } 254 startCannedCancelAnimationnull255 private fun startCannedCancelAnimation() { 256 assertInProgressThread() 257 258 cannedAnimator?.cancel() 259 cannedAnimator = null 260 261 // Temporary remove listener to cancel the spring animation without 262 // finishing the transition 263 springAnimation.removeEndListener(this) 264 springAnimation.cancel() 265 springAnimation.addEndListener(this) 266 267 cannedAnimator = 268 ObjectAnimator.ofFloat(this, AnimationProgressProperty, transitionProgress, 1f).apply { 269 addListener(CannedAnimationListener()) 270 duration = 271 (CANNED_ANIMATION_DURATION_MS.toFloat() * (1f - transitionProgress)).toLong() 272 interpolator = emphasizedInterpolator 273 start() 274 } 275 } 276 277 private inner class CannedAnimationListener : AnimatorListenerAdapter() { onAnimationStartnull278 override fun onAnimationStart(animator: Animator) { 279 Trace.beginAsyncSection("$TAG#cannedAnimatorRunning", 0) 280 } 281 onAnimationEndnull282 override fun onAnimationEnd(animator: Animator) { 283 cancelTransition(1f, animate = false) 284 Trace.endAsyncSection("$TAG#cannedAnimatorRunning", 0) 285 } 286 } 287 288 private object AnimationProgressProperty : 289 FloatProperty<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") { 290 setValuenull291 override fun setValue( 292 provider: PhysicsBasedUnfoldTransitionProgressProvider, 293 value: Float, 294 ) { 295 provider.transitionProgress = value 296 } 297 getnull298 override fun get(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float = 299 provider.transitionProgress 300 } 301 302 private fun assertInProgressThread() { 303 check(progressHandler.looper.isCurrentThread) { 304 val progressThread = progressHandler.looper.thread 305 val thisThread = Thread.currentThread() 306 """should be called from the progress thread. 307 progressThread=$progressThread tid=${progressThread.id} 308 Thread.currentThread()=$thisThread tid=${thisThread.id}""" 309 .trimMargin() 310 } 311 } 312 313 @AssistedFactory 314 interface Factory { createnull315 fun create( 316 foldStateProvider: FoldStateProvider, 317 handler: Handler, 318 ): PhysicsBasedUnfoldTransitionProgressProvider 319 } 320 } 321 322 private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider" 323 private const val DEBUG = true 324 325 private const val USE_CANNED_ANIMATION = true 326 private const val CANNED_ANIMATION_DURATION_MS = 1000 327 private const val SPRING_STIFFNESS = 600.0f 328 private const val MINIMAL_VISIBLE_CHANGE = 0.001f 329 private const val FINAL_HINGE_ANGLE_POSITION = 165f 330