summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/qs/QSLongPressEffectTest.kt327
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt111
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffectViewBinder.kt68
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()
}
}
}