Files
Lawnchair/systemUIUnfold/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt
T
2023-10-21 09:52:01 +08:00

285 lines
10 KiB
Kotlin

/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.unfold.progress
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.os.Trace
import android.util.FloatProperty
import android.util.Log
import android.view.animation.AnimationUtils.loadInterpolator
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN
import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
import com.android.systemui.unfold.updates.name
import javax.inject.Inject
/** Maps fold updates to unfold transition progress using DynamicAnimation. */
class PhysicsBasedUnfoldTransitionProgressProvider
@Inject
constructor(context: Context, private val foldStateProvider: FoldStateProvider) :
UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
private val emphasizedInterpolator =
loadInterpolator(context, android.R.interpolator.fast_out_extra_slow_in)
private var cannedAnimator: ValueAnimator? = null
private val springAnimation =
SpringAnimation(
this,
FloatPropertyCompat.createFloatPropertyCompat(AnimationProgressProperty)
)
.apply { addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider) }
private var isTransitionRunning = false
private var isAnimatedCancelRunning = false
private var transitionProgress: Float = 0.0f
set(value) {
if (isTransitionRunning) {
listeners.forEach { it.onTransitionProgress(value) }
}
field = value
}
private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
init {
foldStateProvider.addCallback(this)
foldStateProvider.start()
}
override fun destroy() {
foldStateProvider.stop()
}
override fun onHingeAngleUpdate(angle: Float) {
if (!isTransitionRunning || isAnimatedCancelRunning) return
val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION)
springAnimation.animateToFinalPosition(progress)
}
private fun saturate(amount: Float, low: Float = 0f, high: Float = 1f): Float =
if (amount < low) low else if (amount > high) high else amount
override fun onFoldUpdate(@FoldUpdate update: Int) {
when (update) {
FOLD_UPDATE_FINISH_FULL_OPEN,
FOLD_UPDATE_FINISH_HALF_OPEN -> {
// Do not cancel if we haven't started the transition yet.
// This could happen when we fully unfolded the device before the screen
// became available. In this case we start and immediately cancel the animation
// in onUnfoldedScreenAvailable event handler, so we don't need to cancel it here.
if (isTransitionRunning) {
cancelTransition(endValue = 1f, animate = true)
}
}
FOLD_UPDATE_FINISH_CLOSED -> {
cancelTransition(endValue = 0f, animate = false)
}
FOLD_UPDATE_START_CLOSING -> {
// The transition might be already running as the device might start closing several
// times before reaching an end state.
if (isTransitionRunning) {
// If we are cancelling the animation, reset that so we can resume it normally.
// The animation could be 'cancelled' when the user stops folding/unfolding
// for some period of time or fully unfolds the device. In this case,
// it is forced to run to the end ignoring all further hinge angle events.
// By resetting this flag we allow reacting to hinge angle events again, so
// the transition continues running.
if (isAnimatedCancelRunning) {
isAnimatedCancelRunning = false
// Switching to spring animation, start the animation if it
// is not running already
springAnimation.animateToFinalPosition(1.0f)
cannedAnimator?.removeAllListeners()
cannedAnimator?.cancel()
cannedAnimator = null
}
} else {
startTransition(startValue = 1f)
}
}
}
if (DEBUG) {
Log.d(TAG, "onFoldUpdate = ${update.name()}")
Trace.setCounter("fold_update", update.toLong())
}
}
override fun onUnfoldedScreenAvailable() {
startTransition(startValue = 0f)
// Stop the animation if the device has already opened by the time when
// the display is available as we won't receive the full open event anymore
if (foldStateProvider.isFinishedOpening) {
cancelTransition(endValue = 1f, animate = true)
}
}
private fun cancelTransition(endValue: Float, animate: Boolean) {
if (isTransitionRunning && animate) {
if (endValue == 1.0f && !isAnimatedCancelRunning) {
listeners.forEach { it.onTransitionFinishing() }
}
isAnimatedCancelRunning = true
if (USE_CANNED_ANIMATION) {
startCannedCancelAnimation()
} else {
springAnimation.animateToFinalPosition(endValue)
}
} else {
transitionProgress = endValue
isAnimatedCancelRunning = false
isTransitionRunning = false
springAnimation.cancel()
cannedAnimator?.removeAllListeners()
cannedAnimator?.cancel()
cannedAnimator = null
listeners.forEach { it.onTransitionFinished() }
if (DEBUG) {
Log.d(TAG, "onTransitionFinished")
}
}
}
override fun onAnimationEnd(
animation: DynamicAnimation<out DynamicAnimation<*>>,
canceled: Boolean,
value: Float,
velocity: Float
) {
if (isAnimatedCancelRunning) {
cancelTransition(value, animate = false)
}
}
private fun onStartTransition() {
Trace.beginSection("$TAG#onStartTransition")
listeners.forEach { it.onTransitionStarted() }
Trace.endSection()
isTransitionRunning = true
if (DEBUG) {
Log.d(TAG, "onTransitionStarted")
}
}
private fun startTransition(startValue: Float) {
if (!isTransitionRunning) onStartTransition()
springAnimation.apply {
spring =
SpringForce().apply {
finalPosition = startValue
dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
stiffness = SPRING_STIFFNESS
}
minimumVisibleChange = MINIMAL_VISIBLE_CHANGE
setStartValue(startValue)
setMinValue(0f)
setMaxValue(1f)
}
springAnimation.start()
}
override fun addCallback(listener: TransitionProgressListener) {
listeners.add(listener)
}
override fun removeCallback(listener: TransitionProgressListener) {
listeners.remove(listener)
}
private fun startCannedCancelAnimation() {
cannedAnimator?.cancel()
cannedAnimator = null
// Temporary remove listener to cancel the spring animation without
// finishing the transition
springAnimation.removeEndListener(this)
springAnimation.cancel()
springAnimation.addEndListener(this)
cannedAnimator =
ObjectAnimator.ofFloat(this, AnimationProgressProperty, transitionProgress, 1f).apply {
addListener(CannedAnimationListener())
duration =
(CANNED_ANIMATION_DURATION_MS.toFloat() * (1f - transitionProgress)).toLong()
interpolator = emphasizedInterpolator
start()
}
}
private inner class CannedAnimationListener : AnimatorListenerAdapter() {
override fun onAnimationStart(animator: Animator) {
Trace.beginAsyncSection("$TAG#cannedAnimatorRunning", 0)
}
override fun onAnimationEnd(animator: Animator) {
cancelTransition(1f, animate = false)
Trace.endAsyncSection("$TAG#cannedAnimatorRunning", 0)
}
}
private object AnimationProgressProperty :
FloatProperty<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") {
override fun setValue(
provider: PhysicsBasedUnfoldTransitionProgressProvider,
value: Float
) {
provider.transitionProgress = value
}
override fun get(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float =
provider.transitionProgress
}
}
private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider"
private const val DEBUG = true
private const val USE_CANNED_ANIMATION = true
private const val CANNED_ANIMATION_DURATION_MS = 1000
private const val SPRING_STIFFNESS = 600.0f
private const val MINIMAL_VISIBLE_CHANGE = 0.001f
private const val FINAL_HINGE_ANGLE_POSITION = 165f