blob: 04d920cb15d5f7cd42602c5774b53a6a0b8aee3b [file] [log] [blame]
/*
* 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.util.Log
import android.util.MathUtils.saturate
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.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
import com.android.systemui.unfold.updates.FoldStateProvider
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
/** Maps fold updates to unfold transition progress using DynamicAnimation. */
class PhysicsBasedUnfoldTransitionProgressProvider(
private val foldStateProvider: FoldStateProvider
) : UnfoldTransitionProgressProvider, FoldUpdatesListener, DynamicAnimation.OnAnimationEndListener {
private val springAnimation =
SpringAnimation(this, 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)
}
override fun onFoldUpdate(@FoldUpdate update: Int) {
when (update) {
FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> {
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)
}
}
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 FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE 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
}
} else {
startTransition(startValue = 1f)
}
}
}
if (DEBUG) {
Log.d(TAG, "onFoldUpdate = $update")
}
}
private fun cancelTransition(endValue: Float, animate: Boolean) {
if (isTransitionRunning && animate) {
isAnimatedCancelRunning = true
springAnimation.animateToFinalPosition(endValue)
} else {
transitionProgress = endValue
isAnimatedCancelRunning = false
isTransitionRunning = false
springAnimation.cancel()
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() {
listeners.forEach { it.onTransitionStarted() }
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 object AnimationProgressProperty :
FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") {
override fun setValue(
provider: PhysicsBasedUnfoldTransitionProgressProvider,
value: Float
) {
provider.transitionProgress = value
}
override fun getValue(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float =
provider.transitionProgress
}
}
private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider"
private const val DEBUG = true
private const val SPRING_STIFFNESS = 200.0f
private const val MINIMAL_VISIBLE_CHANGE = 0.001f
private const val FINAL_HINGE_ANGLE_POSITION = 165f