diff options
3 files changed, 238 insertions, 268 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt index e332656e0f15..c51413a2cc78 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt @@ -21,7 +21,6 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.coroutines.collectLastValue import com.android.systemui.haptics.vibratorHelper import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository @@ -32,19 +31,14 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) class QSLongPressEffectTest : SysuiTestCase() { - @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule() - @get:Rule val animatorTestRule = AnimatorTestRule(this) private val kosmos = testKosmos() private val vibratorHelper = kosmos.vibratorHelper @@ -67,58 +61,28 @@ class QSLongPressEffectTest : SysuiTestCase() { vibratorHelper, kosmos.keyguardInteractor, ) - longPressEffect.initializeEffect(effectDuration) } @Test - fun onReset_whileIdle_resetsEffect() = testWithScope { - // GIVEN a call to reset - longPressEffect.resetEffect() - - // THEN the effect remains idle and has not been initialized - val state by collectLastValue(longPressEffect.state) - assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE) - assertThat(longPressEffect.hasInitialized).isFalse() - } - - @Test - fun onReset_whileRunning_resetsEffect() = testWhileRunning { - // GIVEN a call to reset - longPressEffect.resetEffect() - - // THEN the effect remains idle and has not been initialized - val state by collectLastValue(longPressEffect.state) - assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE) - assertThat(longPressEffect.hasInitialized).isFalse() - } - - @Test - fun onInitialize_withNegativeDuration_doesNotInitialize() = testWithScope { - // GIVEN an effect that has reset - longPressEffect.resetEffect() - - // WHEN attempting to initialize with a negative duration - val couldInitialize = longPressEffect.initializeEffect(-1) - - // THEN the effect can't initialized and remains reset - val state by collectLastValue(longPressEffect.state) - assertThat(couldInitialize).isFalse() - assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE) - assertThat(longPressEffect.hasInitialized).isFalse() - } + fun onInitialize_withNegativeDuration_doesNotInitialize() = + testWithScope(false) { + // WHEN attempting to initialize with a negative duration + val couldInitialize = longPressEffect.initializeEffect(-1) + + // THEN the effect can't initialized and remains reset + assertThat(couldInitialize).isFalse() + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + assertThat(longPressEffect.hasInitialized).isFalse() + } @Test fun onInitialize_withPositiveDuration_initializes() = testWithScope { - // GIVEN an effect that has reset - longPressEffect.resetEffect() - // WHEN attempting to initialize with a positive duration val couldInitialize = longPressEffect.initializeEffect(effectDuration) // THEN the effect is initialized - val state by collectLastValue(longPressEffect.state) assertThat(couldInitialize).isTrue() - assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE) + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) assertThat(longPressEffect.hasInitialized).isTrue() } @@ -128,140 +92,174 @@ class QSLongPressEffectTest : SysuiTestCase() { longPressEffect.handleActionDown() // THEN the effect moves to the TIMEOUT_WAIT state - val state by collectLastValue(longPressEffect.state) - assertThat(state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) } @Test - fun onActionCancel_whileWaiting_goesIdle() = testWhileWaiting { - // GIVEN an action cancel occurs - longPressEffect.handleActionCancel() - - // THEN the effect goes back to idle and does not start - val state by collectLastValue(longPressEffect.state) - assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE) - assertEffectDidNotStart() - } + fun onActionCancel_whileWaiting_goesIdle() = + testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { + // GIVEN an action cancel occurs + longPressEffect.handleActionCancel() + + // THEN the effect goes back to idle and does not start + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + assertEffectDidNotStart() + } @Test - fun onActionUp_whileWaiting_performsClick() = testWhileWaiting { - // GIVEN an action is being collected - val action by collectLastValue(longPressEffect.actionType) + fun onActionUp_whileWaiting_performsClick() = + testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { + // GIVEN an action is being collected + val action by collectLastValue(longPressEffect.actionType) - // GIVEN an action up occurs - longPressEffect.handleActionUp() + // GIVEN an action up occurs + longPressEffect.handleActionUp() - // THEN the action to invoke is the click action and the effect does not start - assertThat(action).isEqualTo(QSLongPressEffect.ActionType.CLICK) - assertEffectDidNotStart() - } + // THEN the action to invoke is the click action and the effect does not start + assertThat(action).isEqualTo(QSLongPressEffect.ActionType.CLICK) + assertEffectDidNotStart() + } + + @Test + fun onWaitComplete_whileWaiting_beginsEffect() = + testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { + // GIVEN the pressed timeout is complete + longPressEffect.handleTimeoutComplete() + + // THEN the effect emits the action to start an animator + val action by collectLastValue(longPressEffect.actionType) + assertThat(action).isEqualTo(QSLongPressEffect.ActionType.START_ANIMATOR) + } @Test - fun onWaitComplete_whileWaiting_beginsEffect() = testWhileWaiting { - // GIVEN the pressed timeout is complete - longPressEffect.handleTimeoutComplete() + fun onAnimationStart_whileWaiting_effectBegins() = + testWhileInState(QSLongPressEffect.State.TIMEOUT_WAIT) { + // GIVEN that the animator starts + longPressEffect.handleAnimationStart() - // THEN the effect starts - assertEffectStarted() - } + // THEN the effect begins + assertEffectStarted() + } @Test - fun onActionUp_whileEffectHasBegun_reversesEffect() = testWhileRunning { - // GIVEN that the effect is at the middle of its completion (progress of 50%) - animatorTestRule.advanceTimeBy(effectDuration / 2L) + fun onActionUp_whileEffectHasBegun_reversesEffect() = + testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { + // GIVEN an action up occurs + longPressEffect.handleActionUp() - // WHEN an action up occurs - longPressEffect.handleActionUp() + // THEN the effect reverses + assertEffectReverses() + } + + @Test + fun onPlayReverseHaptics_reverseHapticsArePlayed() = testWithScope { + // GIVEN a call to play reverse haptics at the effect midpoint + val progress = 0.5f + longPressEffect.playReverseHaptics(progress) - // THEN the effect gets reversed at 50% progress - assertEffectReverses(0.5f) + // THEN the expected texture is played + val reverseHaptics = + LongPressHapticBuilder.createReversedEffect( + progress, + lowTickDuration, + effectDuration, + ) + assertThat(reverseHaptics).isNotNull() + assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue() } @Test - fun onActionCancel_whileEffectHasBegun_reversesEffect() = testWhileRunning { - // GIVEN that the effect is at the middle of its completion (progress of 50%) - animatorTestRule.advanceTimeBy(effectDuration / 2L) - - // WHEN an action cancel occurs - longPressEffect.handleActionCancel() + fun onActionCancel_whileEffectHasBegun_reversesEffect() = + testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { + // WHEN an action cancel occurs + longPressEffect.handleActionCancel() - // THEN the effect gets reversed at 50% progress - assertEffectReverses(0.5f) - } + // THEN the effect gets reversed + assertEffectReverses() + } @Test - fun onAnimationComplete_keyguardDismissible_effectEndsWithLongPress() = testWhileRunning { - // GIVEN that the animation completes - animatorTestRule.advanceTimeBy(effectDuration + 10L) + fun onAnimationComplete_keyguardDismissible_effectEndsWithLongPress() = + testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { + // GIVEN that the animation completes + longPressEffect.handleAnimationComplete() - // THEN the long-press effect completes with a LONG_PRESS - assertEffectCompleted(QSLongPressEffect.ActionType.LONG_PRESS) - } + // THEN the long-press effect completes with a LONG_PRESS + assertEffectCompleted(QSLongPressEffect.ActionType.LONG_PRESS) + } @Test fun onAnimationComplete_keyguardNotDismissible_effectEndsWithResetAndLongPress() = - testWhileRunning { + testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { // GIVEN that the keyguard is not dismissible kosmos.fakeKeyguardRepository.setKeyguardDismissible(false) // GIVEN that the animation completes - animatorTestRule.advanceTimeBy(effectDuration + 10L) + longPressEffect.handleAnimationComplete() // THEN the long-press effect completes with RESET_AND_LONG_PRESS assertEffectCompleted(QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS) } @Test - fun onActionDown_whileRunningBackwards_resets() = testWhileRunning { - // GIVEN that the effect is at the middle of its completion (progress of 50%) - animatorTestRule.advanceTimeBy(effectDuration / 2L) - - // GIVEN an action cancel occurs and the effect gets reversed - longPressEffect.handleActionCancel() + fun onActionDown_whileRunningBackwards_cancels() = + testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { + // GIVEN an action cancel occurs and the effect gets reversed + longPressEffect.handleActionCancel() - // GIVEN an action down occurs - longPressEffect.handleActionDown() + // GIVEN an action down occurs + longPressEffect.handleActionDown() - // THEN the effect resets - assertEffectResets() - } + // THEN the effect posts an action to cancel the animator + val action by collectLastValue(longPressEffect.actionType) + assertThat(action).isEqualTo(QSLongPressEffect.ActionType.CANCEL_ANIMATOR) + } @Test - fun onAnimationComplete_whileRunningBackwards_goesToIdle() = testWhileRunning { - // GIVEN that the effect is at the middle of its completion (progress of 50%) - animatorTestRule.advanceTimeBy(effectDuration / 2L) + fun onAnimatorCancel_effectGoesBackToWait() = + testWhileInState(QSLongPressEffect.State.RUNNING_FORWARD) { + // GIVEN that the animator was cancelled + longPressEffect.handleAnimationCancel() - // GIVEN an action cancel occurs and the effect gets reversed - longPressEffect.handleActionCancel() + // THEN the state goes to the timeout wait + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) + } - // GIVEN that the animation completes after a sufficient amount of time - animatorTestRule.advanceTimeBy(effectDuration.toLong()) + @Test + fun onAnimationComplete_whileRunningBackwards_goesToIdle() = + testWhileInState(QSLongPressEffect.State.RUNNING_BACKWARDS) { + // GIVEN an action cancel occurs and the effect gets reversed + longPressEffect.handleActionCancel() - // THEN the state goes to [QSLongPressEffect.State.IDLE] - val state by collectLastValue(longPressEffect.state) - assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE) - } + // GIVEN that the animation completes + longPressEffect.handleAnimationComplete() - private fun testWithScope(test: suspend TestScope.() -> Unit) = - with(kosmos) { testScope.runTest { test() } } + // THEN the state goes to [QSLongPressEffect.State.IDLE] + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) + } - private fun testWhileWaiting(test: suspend TestScope.() -> Unit) = + private fun testWithScope(initialize: Boolean = true, test: suspend TestScope.() -> Unit) = with(kosmos) { testScope.runTest { - // GIVEN the TIMEOUT_WAIT state is entered - longPressEffect.setState(QSLongPressEffect.State.TIMEOUT_WAIT) - - // THEN run the test + if (initialize) { + longPressEffect.initializeEffect(effectDuration) + } test() } } - private fun testWhileRunning(test: suspend TestScope.() -> Unit) = + private fun testWhileInState( + state: QSLongPressEffect.State, + initialize: Boolean = true, + test: suspend TestScope.() -> Unit, + ) = with(kosmos) { testScope.runTest { - // GIVEN that the effect starts after the tap timeout is complete - longPressEffect.setState(QSLongPressEffect.State.TIMEOUT_WAIT) - longPressEffect.handleTimeoutComplete() + if (initialize) { + longPressEffect.initializeEffect(effectDuration) + } + // GIVEN a state + longPressEffect.setState(state) // THEN run the test test() @@ -270,13 +268,10 @@ class QSLongPressEffectTest : SysuiTestCase() { /** * Asserts that the effect started by checking that: - * 1. The effect progress is 0f - * 2. Initial hint haptics are played - * 3. The internal state is [QSLongPressEffect.State.RUNNING_FORWARD] + * 1. Initial hint haptics are played + * 2. The internal state is [QSLongPressEffect.State.RUNNING_FORWARD] */ - private fun TestScope.assertEffectStarted() { - val effectProgress by collectLastValue(longPressEffect.effectProgress) - val state by collectLastValue(longPressEffect.state) + private fun assertEffectStarted() { val longPressHint = LongPressHapticBuilder.createLongPressHint( lowTickDuration, @@ -284,78 +279,48 @@ class QSLongPressEffectTest : SysuiTestCase() { effectDuration, ) - assertThat(state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD) - assertThat(effectProgress).isEqualTo(0f) + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD) assertThat(longPressHint).isNotNull() assertThat(vibratorHelper.hasVibratedWithEffects(longPressHint!!)).isTrue() } /** * Asserts that the effect did not start by checking that: - * 1. No effect progress is emitted - * 2. No haptics are played - * 3. The internal state is not [QSLongPressEffect.State.RUNNING_BACKWARDS] or + * 1. No haptics are played + * 2. The internal state is not [QSLongPressEffect.State.RUNNING_BACKWARDS] or * [QSLongPressEffect.State.RUNNING_FORWARD] */ - private fun TestScope.assertEffectDidNotStart() { - val effectProgress by collectLastValue(longPressEffect.effectProgress) - val state by collectLastValue(longPressEffect.state) - - assertThat(state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD) - assertThat(state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS) - assertThat(effectProgress).isNull() + private fun assertEffectDidNotStart() { + assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD) + assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS) assertThat(vibratorHelper.totalVibrations).isEqualTo(0) } /** * Asserts that the effect completes by checking that: - * 1. The progress is null - * 2. The final snap haptics are played - * 3. The internal state goes back to [QSLongPressEffect.State.IDLE] - * 4. The action to perform on the tile is the action given as a parameter + * 1. The final snap haptics are played + * 2. The internal state goes back to [QSLongPressEffect.State.IDLE] + * 3. The action to perform on the tile is the action given as a parameter */ private fun TestScope.assertEffectCompleted(expectedAction: QSLongPressEffect.ActionType) { val action by collectLastValue(longPressEffect.actionType) - val effectProgress by collectLastValue(longPressEffect.effectProgress) val snapEffect = LongPressHapticBuilder.createSnapEffect() - val state by collectLastValue(longPressEffect.state) - assertThat(effectProgress).isNull() assertThat(snapEffect).isNotNull() assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue() - assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE) + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE) assertThat(action).isEqualTo(expectedAction) } /** * Assert that the effect gets reverted by checking that: * 1. The internal state is [QSLongPressEffect.State.RUNNING_BACKWARDS] - * 2. The reverse haptics plays at the point where the animation was paused + * 2. An action to reverse the animator is emitted */ - private fun TestScope.assertEffectReverses(pausedProgress: Float) { - val reverseHaptics = - LongPressHapticBuilder.createReversedEffect( - pausedProgress, - lowTickDuration, - effectDuration, - ) - val state by collectLastValue(longPressEffect.state) - - assertThat(state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS) - assertThat(reverseHaptics).isNotNull() - assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue() - } - - /** - * Asserts that the effect resets by checking that: - * 1. The effect progress resets to 0 - * 2. The internal state goes back to [QSLongPressEffect.State.TIMEOUT_WAIT] - */ - private fun TestScope.assertEffectResets() { - val effectProgress by collectLastValue(longPressEffect.effectProgress) - val state by collectLastValue(longPressEffect.state) + private fun TestScope.assertEffectReverses() { + val action by collectLastValue(longPressEffect.actionType) - assertThat(effectProgress).isNull() - assertThat(state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT) + assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS) + assertThat(action).isEqualTo(QSLongPressEffect.ActionType.REVERSE_ANIMATOR) } } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index f1a8faf87417..db2ec8f27cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -16,20 +16,14 @@ package com.android.systemui.haptics.qs -import android.animation.ValueAnimator import android.os.VibrationEffect import android.view.View -import android.view.animation.AccelerateDecelerateInterpolator import androidx.annotation.VisibleForTesting -import androidx.core.animation.doOnCancel -import androidx.core.animation.doOnEnd -import androidx.core.animation.doOnStart import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.statusbar.VibratorHelper import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine /** @@ -50,17 +44,14 @@ constructor( keyguardInteractor: KeyguardInteractor, ) { - private var effectDuration = 0 + var effectDuration = 0 + private set /** Current state */ - private var _state = MutableStateFlow(State.IDLE) - val state = _state.asStateFlow() + var state = State.IDLE + private set - /** Flows for view control and action */ - private val _effectProgress = MutableStateFlow<Float?>(null) - val effectProgress = _effectProgress.asStateFlow() - - // Actions to perform + /** Flow for view control and action */ private val _postedActionType = MutableStateFlow<ActionType?>(null) val actionType: Flow<ActionType?> = combine( @@ -85,29 +76,23 @@ constructor( private val snapEffect = LongPressHapticBuilder.createSnapEffect() - private var effectAnimator: ValueAnimator? = null - val hasInitialized: Boolean - get() = longPressHint != null && effectAnimator != null + get() = longPressHint != null @VisibleForTesting - fun setState(state: State) { - _state.value = state + fun setState(newState: State) { + state = newState } - private fun reverse() { - effectAnimator?.let { - val pausedProgress = it.animatedFraction - val effect = - LongPressHapticBuilder.createReversedEffect( - pausedProgress, - durations?.get(0) ?: 0, - effectDuration, - ) - vibratorHelper?.cancel() - vibrate(effect) - it.reverse() - } + fun playReverseHaptics(pausedProgress: Float) { + val effect = + LongPressHapticBuilder.createReversedEffect( + pausedProgress, + durations?.get(0) ?: 0, + effectDuration, + ) + vibratorHelper?.cancel() + vibrate(effect) } private fun vibrate(effect: VibrationEffect?) { @@ -117,23 +102,23 @@ constructor( } fun handleActionDown() { - when (_state.value) { + when (state) { State.IDLE -> { setState(State.TIMEOUT_WAIT) } - State.RUNNING_BACKWARDS -> effectAnimator?.cancel() + State.RUNNING_BACKWARDS -> _postedActionType.value = ActionType.CANCEL_ANIMATOR else -> {} } } fun handleActionUp() { - when (_state.value) { + when (state) { State.TIMEOUT_WAIT -> { _postedActionType.value = ActionType.CLICK setState(State.IDLE) } State.RUNNING_FORWARD -> { - reverse() + _postedActionType.value = ActionType.REVERSE_ANIMATOR setState(State.RUNNING_BACKWARDS) } else -> {} @@ -141,44 +126,42 @@ constructor( } fun handleActionCancel() { - when (_state.value) { + when (state) { State.TIMEOUT_WAIT -> { setState(State.IDLE) } State.RUNNING_FORWARD -> { - reverse() + _postedActionType.value = ActionType.REVERSE_ANIMATOR setState(State.RUNNING_BACKWARDS) } else -> {} } } - private fun handleAnimationStart() { + fun handleAnimationStart() { vibrate(longPressHint) setState(State.RUNNING_FORWARD) } /** This function is called both when an animator completes or gets cancelled */ - private fun handleAnimationComplete() { - if (_state.value == State.RUNNING_FORWARD) { + fun handleAnimationComplete() { + if (state == State.RUNNING_FORWARD) { vibrate(snapEffect) _postedActionType.value = ActionType.LONG_PRESS - _effectProgress.value = null } - if (_state.value != State.TIMEOUT_WAIT) { + if (state != State.TIMEOUT_WAIT) { // This will happen if the animator did not finish by being cancelled setState(State.IDLE) } } - private fun handleAnimationCancel() { - _effectProgress.value = null + fun handleAnimationCancel() { setState(State.TIMEOUT_WAIT) } fun handleTimeoutComplete() { - if (_state.value == State.TIMEOUT_WAIT && effectAnimator?.isRunning == false) { - effectAnimator?.start() + if (state == State.TIMEOUT_WAIT) { + _postedActionType.value = ActionType.START_ANIMATOR } } @@ -186,18 +169,6 @@ constructor( _postedActionType.value = null } - /** Reset the effect by going back to a default [IDLE] state */ - fun resetEffect() { - if (effectAnimator?.isRunning == true) { - effectAnimator?.cancel() - } - longPressHint = null - effectAnimator = null - _effectProgress.value = null - _postedActionType.value = null - setState(State.IDLE) - } - /** * Reset the effect with a new effect duration. * @@ -205,27 +176,21 @@ constructor( * @return true if the effect initialized correctly */ fun initializeEffect(duration: Int): Boolean { - // The effect can't reset if it is running + // The effect can't initialize with a negative duration if (duration <= 0) return false - resetEffect() + // There is no need to re-initialize if the duration has not changed + if (duration == effectDuration) return true + effectDuration = duration - effectAnimator = - ValueAnimator.ofFloat(0f, 1f).apply { - this.duration = effectDuration.toLong() - interpolator = AccelerateDecelerateInterpolator() - - doOnStart { handleAnimationStart() } - addUpdateListener { _effectProgress.value = animatedValue as Float } - doOnEnd { handleAnimationComplete() } - doOnCancel { handleAnimationCancel() } - } longPressHint = LongPressHapticBuilder.createLongPressHint( durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION, durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION, effectDuration ) + _postedActionType.value = ActionType.INITIALIZE_ANIMATOR + setState(State.IDLE) return true } @@ -241,5 +206,9 @@ constructor( CLICK, LONG_PRESS, RESET_AND_LONG_PRESS, + START_ANIMATOR, + REVERSE_ANIMATOR, + CANCEL_ANIMATOR, + INITIALIZE_ANIMATOR, } } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt index dd7a285eeaae..c591af2ef09f 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt @@ -16,9 +16,14 @@ package com.android.systemui.haptics.qs +import android.animation.ValueAnimator import android.annotation.SuppressLint import android.view.MotionEvent import android.view.ViewConfiguration +import android.view.animation.AccelerateDecelerateInterpolator +import androidx.core.animation.doOnCancel +import androidx.core.animation.doOnEnd +import androidx.core.animation.doOnStart import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launch @@ -40,32 +45,63 @@ object QSLongPressEffectViewBinder { return tile.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { - // Progress of the effect - launch({ "${tileSpec ?: "unknownTileSpec"}#LongPressEffect#progress" }) { - qsLongPressEffect.effectProgress.collect { progress -> - progress?.let { - if (it == 0f) { - tile.bringToFront() - } else { - tile.updateLongPressEffectProperties(it) - } - } - } - } - // Action to perform launch({ "${tileSpec ?: "unknownTileSpec"}#LongPressEffect#action" }) { + var effectAnimator: ValueAnimator? = null + qsLongPressEffect.actionType.collect { action -> action?.let { when (it) { - QSLongPressEffect.ActionType.CLICK -> tile.performClick() - QSLongPressEffect.ActionType.LONG_PRESS -> tile.performLongClick() + QSLongPressEffect.ActionType.CLICK -> { + tile.performClick() + qsLongPressEffect.clearActionType() + } + QSLongPressEffect.ActionType.LONG_PRESS -> { + tile.performLongClick() + qsLongPressEffect.clearActionType() + } QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS -> { tile.resetLongPressEffectProperties() tile.performLongClick() + qsLongPressEffect.clearActionType() + } + QSLongPressEffect.ActionType.START_ANIMATOR -> { + if (effectAnimator?.isRunning == false) { + effectAnimator?.start() + } + } + QSLongPressEffect.ActionType.REVERSE_ANIMATOR -> { + effectAnimator?.let { + val pausedProgress = it.animatedFraction + qsLongPressEffect.playReverseHaptics(pausedProgress) + it.reverse() + } + } + QSLongPressEffect.ActionType.CANCEL_ANIMATOR -> { + tile.resetLongPressEffectProperties() + effectAnimator?.cancel() + } + QSLongPressEffect.ActionType.INITIALIZE_ANIMATOR -> { + effectAnimator = + ValueAnimator.ofFloat(0f, 1f).apply { + this.duration = + qsLongPressEffect.effectDuration.toLong() + interpolator = AccelerateDecelerateInterpolator() + + doOnStart { qsLongPressEffect.handleAnimationStart() } + addUpdateListener { + val value = animatedValue as Float + if (value == 0f) { + tile.bringToFront() + } else { + tile.updateLongPressEffectProperties(value) + } + } + doOnEnd { qsLongPressEffect.handleAnimationComplete() } + doOnCancel { qsLongPressEffect.handleAnimationCancel() } + } } } - qsLongPressEffect.clearActionType() } } } |