Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +45 −2 Original line number Diff line number Diff line Loading @@ -316,14 +316,57 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { } @Test fun isMagneticRowDismissible_isDismissibleWhenDetached() = fun isMagneticRowDismissible_whenDetached_isDismissibleWithCorrectDirection_andFastFling() = kosmos.testScope.runTest { setDetachedState() val isDismissible = underTest.isMagneticRowSwipeDetached(swipedRow) // With a fast enough velocity in the direction of the detachment val velocity = Float.POSITIVE_INFINITY // The row is dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isTrue() } @Test fun isMagneticRowDismissible_whenDetached_isDismissibleWithCorrectDirection_andSlowFling() = kosmos.testScope.runTest { setDetachedState() // With a very low velocity in the direction of the detachment val velocity = 0.01f // The row is dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isTrue() } @Test fun isMagneticRowDismissible_whenDetached_isDismissibleWithOppositeDirection_andSlowFling() = kosmos.testScope.runTest { setDetachedState() // With a very low velocity in the opposite direction relative to the detachment val velocity = -0.01f // The row is dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isTrue() } @Test fun isMagneticRowDismissible_whenDetached_isNotDismissibleWithOppositeDirection_andFastFling() = kosmos.testScope.runTest { setDetachedState() // With a high enough velocity in the opposite direction relative to the detachment val velocity = Float.NEGATIVE_INFINITY // The row is not dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isFalse() } @Test fun setMagneticRowTranslation_whenDetached_belowAttachThreshold_reattaches() = kosmos.testScope.runTest { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java +2 −1 Original line number Diff line number Diff line Loading @@ -339,6 +339,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { verify(mSwipeHelper, never()).isFalseGesture(); } @DisableFlags(Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES) @Test public void testIsDismissGesture() { doReturn(false).when(mSwipeHelper).isFalseGesture(); Loading Loading @@ -382,7 +383,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { doReturn(false).when(mSwipeHelper).isFalseGesture(); doReturn(false).when(mSwipeHelper).swipedFarEnough(); doReturn(false).when(mSwipeHelper).swipedFastEnough(); doReturn(true).when(mCallback).isMagneticViewDetached(any()); doReturn(true).when(mCallback).isMagneticViewDismissible(any(), anyFloat()); when(mCallback.canChildBeDismissedInDirection(any(), anyBoolean())).thenReturn(true); when(mEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_UP); Loading packages/SystemUI/src/com/android/systemui/SwipeHelper.java +8 −4 Original line number Diff line number Diff line Loading @@ -792,7 +792,8 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { /** Can the swipe gesture on the touched view be considered as a dismiss intention */ public boolean isSwipeDismissible() { if (magneticNotificationSwipes()) { return mCallback.isMagneticViewDetached(mTouchedView) || swipedFastEnough(); float velocity = getVelocity(mVelocityTracker); return mCallback.isMagneticViewDismissible(mTouchedView, velocity); } else { return swipedFastEnough() || swipedFarEnough(); } Loading Loading @@ -978,11 +979,14 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { void onMagneticInteractionEnd(View view, float velocity); /** * Determine if a view managed by magnetic interactions is magnetically detached * Determine if a view managed by magnetic interactions is dismissible when being swiped by * a touch drag gesture. * * @param view The magnetic view * @return if the view is detached according to its magnetic state. * @param endVelocity The velocity of the drag that is moving the magnetic view * @return if the view is dismissible according to its magnetic logic. */ boolean isMagneticViewDetached(View view); boolean isMagneticViewDismissible(View view, float endVelocity); /** * Called when the child is long pressed and available to start drag and drop. Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt +7 −4 Original line number Diff line number Diff line Loading @@ -87,8 +87,10 @@ interface MagneticNotificationRowManager { */ fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float? = null) /** Determine if the given [ExpandableNotificationRow] has been magnetically detached. */ fun isMagneticRowSwipeDetached(row: ExpandableNotificationRow): Boolean /** * Determine if a magnetic row swiped is dismissible according to the end velocity of the swipe. */ fun isMagneticRowSwipedDismissible(row: ExpandableNotificationRow, endVelocity: Float): Boolean /* Reset any roundness that magnetic targets may have */ fun resetRoundness() Loading Loading @@ -133,8 +135,9 @@ interface MagneticNotificationRowManager { velocity: Float?, ) {} override fun isMagneticRowSwipeDetached( row: ExpandableNotificationRow override fun isMagneticRowSwipedDismissible( row: ExpandableNotificationRow, endVelocity: Float, ): Boolean = false override fun resetRoundness() {} Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +95 −2 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import com.google.android.msdl.domain.MSDLPlayer import javax.inject.Inject import kotlin.math.abs import kotlin.math.pow import kotlin.math.sign import org.jetbrains.annotations.TestOnly @SysUISingleton Loading Loading @@ -72,11 +73,16 @@ constructor( */ private var translationOffset = 0f private var dismissVelocity = 0f private val detachDirectionEstimator = DirectionEstimator() override fun onDensityChange(density: Float) { magneticDetachThreshold = density * MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP magneticAttachThreshold = density * MagneticNotificationRowManager.MAGNETIC_ATTACH_THRESHOLD_DP dismissVelocity = density * DISMISS_VELOCITY } override fun setMagneticAndRoundableTargets( Loading @@ -86,6 +92,7 @@ constructor( ) { if (currentState == State.IDLE) { translationOffset = 0f detachDirectionEstimator.reset() updateMagneticAndRoundableTargets(swipingRow, stackScrollLayout, sectionsManager) currentState = State.TARGETS_SET } else { Loading Loading @@ -142,10 +149,12 @@ constructor( return false } State.TARGETS_SET -> { detachDirectionEstimator.recordTranslation(correctedTranslation) pullTargets(correctedTranslation, canTargetBeDismissed) currentState = State.PULLING } State.PULLING -> { detachDirectionEstimator.recordTranslation(correctedTranslation) updateRoundness(correctedTranslation) if (canTargetBeDismissed) { pullDismissibleRow(correctedTranslation) Loading @@ -154,6 +163,7 @@ constructor( } } State.DETACHED -> { detachDirectionEstimator.recordTranslation(correctedTranslation) translateDetachedRow(correctedTranslation) } } Loading @@ -171,6 +181,7 @@ constructor( private fun pullDismissibleRow(translation: Float) { val crossedThreshold = abs(translation) >= magneticDetachThreshold if (crossedThreshold) { detachDirectionEstimator.halt() snapNeighborsBack() currentMagneticListeners.swipedListener()?.let { detach(it, translation) } currentState = State.DETACHED Loading Loading @@ -249,6 +260,7 @@ constructor( val crossedThreshold = abs(translation) <= magneticAttachThreshold if (crossedThreshold) { translationOffset += translation detachDirectionEstimator.reset() updateRoundness(translation = 0f, animate = true) currentMagneticListeners.swipedListener()?.let { attach(it) } currentState = State.PULLING Loading @@ -266,6 +278,7 @@ constructor( override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) { translationOffset = 0f detachDirectionEstimator.reset() if (row.isSwipedTarget()) { when (currentState) { State.TARGETS_SET -> currentState = State.IDLE Loading @@ -288,13 +301,28 @@ constructor( } } override fun isMagneticRowSwipeDetached(row: ExpandableNotificationRow): Boolean = row.isSwipedTarget() && currentState == State.DETACHED override fun isMagneticRowSwipedDismissible( row: ExpandableNotificationRow, endVelocity: Float, ): Boolean { if (!row.isSwipedTarget()) return false val isEndVelocityLargeEnough = abs(endVelocity) >= dismissVelocity val shouldSnapBack = isEndVelocityLargeEnough && detachDirectionEstimator.direction != sign(endVelocity) return when (currentState) { State.IDLE, State.TARGETS_SET, State.PULLING -> isEndVelocityLargeEnough State.DETACHED -> !shouldSnapBack } } override fun resetRoundness() = notificationRoundnessManager.clear() override fun reset() { translationOffset = 0f detachDirectionEstimator.reset() currentMagneticListeners.forEach { it?.cancelMagneticAnimations() it?.cancelTranslationAnimations() Loading @@ -315,6 +343,69 @@ constructor( private fun NotificationRoundnessManager.setRoundableTargets(targets: RoundableTargets) = setViewsAffectedBySwipe(targets.before, targets.swiped, targets.after) /** * A class to estimate the direction of a gesture translations with a moving average. * * The class holds a buffer that stores translations. When requested, the direction of movement * is estimated as the sign of the average value from the buffer. */ class DirectionEstimator { // A buffer to hold past translations. This is used as a FIFO structure with a fixed size. private val translationBuffer = ArrayDeque<Float>() /** * The estimated direction of the translations. It will be estimated as the average of the * values in the [translationBuffer] and set only once when the estimator is halted. */ var direction = 0f private set private var acceptTranslations = true /** * Add a new translation to the [translationBuffer] if we are still accepting translations * (see [halt]). If the buffer is full, we remove the last value and add the new one to the * end. */ fun recordTranslation(translation: Float) { if (!acceptTranslations) return if (translationBuffer.size == TRANSLATION_BUFFER_SIZE) { translationBuffer.removeFirst() } translationBuffer.addLast(translation) } /** * Halt the operation of the estimator. * * This stops the estimator from receiving new translations and derives the estimated * direction. This is the sign of the average value from the available data in the * [translationBuffer]. */ fun halt() { acceptTranslations = false direction = translationBuffer.mean() } fun reset() { translationBuffer.clear() acceptTranslations = true } private fun ArrayDeque<Float>.mean(): Float = if (isEmpty()) { 0f } else { sign(sum() / translationBuffer.size) } companion object { private const val TRANSLATION_BUFFER_SIZE = 10 } } enum class State { IDLE, TARGETS_SET, Loading @@ -341,6 +432,8 @@ constructor( private const val ATTACH_STIFFNESS = 800f private const val ATTACH_DAMPING_RATIO = 0.95f private const val DISMISS_VELOCITY = 500 // in dp/sec // Maximum value of corner roundness that gets applied during the pre-detach dragging private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt +45 −2 Original line number Diff line number Diff line Loading @@ -316,14 +316,57 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() { } @Test fun isMagneticRowDismissible_isDismissibleWhenDetached() = fun isMagneticRowDismissible_whenDetached_isDismissibleWithCorrectDirection_andFastFling() = kosmos.testScope.runTest { setDetachedState() val isDismissible = underTest.isMagneticRowSwipeDetached(swipedRow) // With a fast enough velocity in the direction of the detachment val velocity = Float.POSITIVE_INFINITY // The row is dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isTrue() } @Test fun isMagneticRowDismissible_whenDetached_isDismissibleWithCorrectDirection_andSlowFling() = kosmos.testScope.runTest { setDetachedState() // With a very low velocity in the direction of the detachment val velocity = 0.01f // The row is dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isTrue() } @Test fun isMagneticRowDismissible_whenDetached_isDismissibleWithOppositeDirection_andSlowFling() = kosmos.testScope.runTest { setDetachedState() // With a very low velocity in the opposite direction relative to the detachment val velocity = -0.01f // The row is dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isTrue() } @Test fun isMagneticRowDismissible_whenDetached_isNotDismissibleWithOppositeDirection_andFastFling() = kosmos.testScope.runTest { setDetachedState() // With a high enough velocity in the opposite direction relative to the detachment val velocity = Float.NEGATIVE_INFINITY // The row is not dismissible val isDismissible = underTest.isMagneticRowSwipedDismissible(swipedRow, velocity) assertThat(isDismissible).isFalse() } @Test fun setMagneticRowTranslation_whenDetached_belowAttachThreshold_reattaches() = kosmos.testScope.runTest { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java +2 −1 Original line number Diff line number Diff line Loading @@ -339,6 +339,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { verify(mSwipeHelper, never()).isFalseGesture(); } @DisableFlags(Flags.FLAG_MAGNETIC_NOTIFICATION_SWIPES) @Test public void testIsDismissGesture() { doReturn(false).when(mSwipeHelper).isFalseGesture(); Loading Loading @@ -382,7 +383,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase { doReturn(false).when(mSwipeHelper).isFalseGesture(); doReturn(false).when(mSwipeHelper).swipedFarEnough(); doReturn(false).when(mSwipeHelper).swipedFastEnough(); doReturn(true).when(mCallback).isMagneticViewDetached(any()); doReturn(true).when(mCallback).isMagneticViewDismissible(any(), anyFloat()); when(mCallback.canChildBeDismissedInDirection(any(), anyBoolean())).thenReturn(true); when(mEvent.getActionMasked()).thenReturn(MotionEvent.ACTION_UP); Loading
packages/SystemUI/src/com/android/systemui/SwipeHelper.java +8 −4 Original line number Diff line number Diff line Loading @@ -792,7 +792,8 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { /** Can the swipe gesture on the touched view be considered as a dismiss intention */ public boolean isSwipeDismissible() { if (magneticNotificationSwipes()) { return mCallback.isMagneticViewDetached(mTouchedView) || swipedFastEnough(); float velocity = getVelocity(mVelocityTracker); return mCallback.isMagneticViewDismissible(mTouchedView, velocity); } else { return swipedFastEnough() || swipedFarEnough(); } Loading Loading @@ -978,11 +979,14 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { void onMagneticInteractionEnd(View view, float velocity); /** * Determine if a view managed by magnetic interactions is magnetically detached * Determine if a view managed by magnetic interactions is dismissible when being swiped by * a touch drag gesture. * * @param view The magnetic view * @return if the view is detached according to its magnetic state. * @param endVelocity The velocity of the drag that is moving the magnetic view * @return if the view is dismissible according to its magnetic logic. */ boolean isMagneticViewDetached(View view); boolean isMagneticViewDismissible(View view, float endVelocity); /** * Called when the child is long pressed and available to start drag and drop. Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt +7 −4 Original line number Diff line number Diff line Loading @@ -87,8 +87,10 @@ interface MagneticNotificationRowManager { */ fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float? = null) /** Determine if the given [ExpandableNotificationRow] has been magnetically detached. */ fun isMagneticRowSwipeDetached(row: ExpandableNotificationRow): Boolean /** * Determine if a magnetic row swiped is dismissible according to the end velocity of the swipe. */ fun isMagneticRowSwipedDismissible(row: ExpandableNotificationRow, endVelocity: Float): Boolean /* Reset any roundness that magnetic targets may have */ fun resetRoundness() Loading Loading @@ -133,8 +135,9 @@ interface MagneticNotificationRowManager { velocity: Float?, ) {} override fun isMagneticRowSwipeDetached( row: ExpandableNotificationRow override fun isMagneticRowSwipedDismissible( row: ExpandableNotificationRow, endVelocity: Float, ): Boolean = false override fun resetRoundness() {} Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +95 −2 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import com.google.android.msdl.domain.MSDLPlayer import javax.inject.Inject import kotlin.math.abs import kotlin.math.pow import kotlin.math.sign import org.jetbrains.annotations.TestOnly @SysUISingleton Loading Loading @@ -72,11 +73,16 @@ constructor( */ private var translationOffset = 0f private var dismissVelocity = 0f private val detachDirectionEstimator = DirectionEstimator() override fun onDensityChange(density: Float) { magneticDetachThreshold = density * MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP magneticAttachThreshold = density * MagneticNotificationRowManager.MAGNETIC_ATTACH_THRESHOLD_DP dismissVelocity = density * DISMISS_VELOCITY } override fun setMagneticAndRoundableTargets( Loading @@ -86,6 +92,7 @@ constructor( ) { if (currentState == State.IDLE) { translationOffset = 0f detachDirectionEstimator.reset() updateMagneticAndRoundableTargets(swipingRow, stackScrollLayout, sectionsManager) currentState = State.TARGETS_SET } else { Loading Loading @@ -142,10 +149,12 @@ constructor( return false } State.TARGETS_SET -> { detachDirectionEstimator.recordTranslation(correctedTranslation) pullTargets(correctedTranslation, canTargetBeDismissed) currentState = State.PULLING } State.PULLING -> { detachDirectionEstimator.recordTranslation(correctedTranslation) updateRoundness(correctedTranslation) if (canTargetBeDismissed) { pullDismissibleRow(correctedTranslation) Loading @@ -154,6 +163,7 @@ constructor( } } State.DETACHED -> { detachDirectionEstimator.recordTranslation(correctedTranslation) translateDetachedRow(correctedTranslation) } } Loading @@ -171,6 +181,7 @@ constructor( private fun pullDismissibleRow(translation: Float) { val crossedThreshold = abs(translation) >= magneticDetachThreshold if (crossedThreshold) { detachDirectionEstimator.halt() snapNeighborsBack() currentMagneticListeners.swipedListener()?.let { detach(it, translation) } currentState = State.DETACHED Loading Loading @@ -249,6 +260,7 @@ constructor( val crossedThreshold = abs(translation) <= magneticAttachThreshold if (crossedThreshold) { translationOffset += translation detachDirectionEstimator.reset() updateRoundness(translation = 0f, animate = true) currentMagneticListeners.swipedListener()?.let { attach(it) } currentState = State.PULLING Loading @@ -266,6 +278,7 @@ constructor( override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) { translationOffset = 0f detachDirectionEstimator.reset() if (row.isSwipedTarget()) { when (currentState) { State.TARGETS_SET -> currentState = State.IDLE Loading @@ -288,13 +301,28 @@ constructor( } } override fun isMagneticRowSwipeDetached(row: ExpandableNotificationRow): Boolean = row.isSwipedTarget() && currentState == State.DETACHED override fun isMagneticRowSwipedDismissible( row: ExpandableNotificationRow, endVelocity: Float, ): Boolean { if (!row.isSwipedTarget()) return false val isEndVelocityLargeEnough = abs(endVelocity) >= dismissVelocity val shouldSnapBack = isEndVelocityLargeEnough && detachDirectionEstimator.direction != sign(endVelocity) return when (currentState) { State.IDLE, State.TARGETS_SET, State.PULLING -> isEndVelocityLargeEnough State.DETACHED -> !shouldSnapBack } } override fun resetRoundness() = notificationRoundnessManager.clear() override fun reset() { translationOffset = 0f detachDirectionEstimator.reset() currentMagneticListeners.forEach { it?.cancelMagneticAnimations() it?.cancelTranslationAnimations() Loading @@ -315,6 +343,69 @@ constructor( private fun NotificationRoundnessManager.setRoundableTargets(targets: RoundableTargets) = setViewsAffectedBySwipe(targets.before, targets.swiped, targets.after) /** * A class to estimate the direction of a gesture translations with a moving average. * * The class holds a buffer that stores translations. When requested, the direction of movement * is estimated as the sign of the average value from the buffer. */ class DirectionEstimator { // A buffer to hold past translations. This is used as a FIFO structure with a fixed size. private val translationBuffer = ArrayDeque<Float>() /** * The estimated direction of the translations. It will be estimated as the average of the * values in the [translationBuffer] and set only once when the estimator is halted. */ var direction = 0f private set private var acceptTranslations = true /** * Add a new translation to the [translationBuffer] if we are still accepting translations * (see [halt]). If the buffer is full, we remove the last value and add the new one to the * end. */ fun recordTranslation(translation: Float) { if (!acceptTranslations) return if (translationBuffer.size == TRANSLATION_BUFFER_SIZE) { translationBuffer.removeFirst() } translationBuffer.addLast(translation) } /** * Halt the operation of the estimator. * * This stops the estimator from receiving new translations and derives the estimated * direction. This is the sign of the average value from the available data in the * [translationBuffer]. */ fun halt() { acceptTranslations = false direction = translationBuffer.mean() } fun reset() { translationBuffer.clear() acceptTranslations = true } private fun ArrayDeque<Float>.mean(): Float = if (isEmpty()) { 0f } else { sign(sum() / translationBuffer.size) } companion object { private const val TRANSLATION_BUFFER_SIZE = 10 } } enum class State { IDLE, TARGETS_SET, Loading @@ -341,6 +432,8 @@ constructor( private const val ATTACH_STIFFNESS = 800f private const val ATTACH_DAMPING_RATIO = 0.95f private const val DISMISS_VELOCITY = 500 // in dp/sec // Maximum value of corner roundness that gets applied during the pre-detach dragging private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f Loading