diff options
author | 2025-03-21 11:21:12 -0700 | |
---|---|---|
committer | 2025-03-24 10:13:34 -0700 | |
commit | b9b6d74e153859d710946df9f0ac2b3b2c001564 (patch) | |
tree | 3803d1f8fad75f23566d6349919b0669ae79ab61 /packages/SystemUI/src | |
parent | 4df491445a5e9da47a58e20edd46c4ec8b280319 (diff) |
Modifiying the rules that determine magnetic dismissibility
To avoid accidental dismissals, we also consider the magnitude and
direction of velocity to determine if a notification is dismissible
after it has been magnetically detached from its neighbors. When
detached, a notification snaps back when there is a fast enough fling in
the opposite direction relative to the detachment direction. In any
other case, the notification dismisses when let go.
Test: modified and added new Unit tests
Flag: com.android.systemui.magnetic_notification_swipes
Bug: 405126597
Change-Id: Ic4c9074d2dc2e995c1b28b11529aa97395d1144e
Diffstat (limited to 'packages/SystemUI/src')
5 files changed, 115 insertions, 16 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index d017754ae653..b131534fe818 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -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(); } @@ -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. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt index 48cff7497e3c..9bd9a0bb0089 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManager.kt @@ -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() @@ -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() {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt index 5c52500b7f70..03ede2fdc12f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt @@ -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 @@ -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( @@ -86,6 +92,7 @@ constructor( ) { if (currentState == State.IDLE) { translationOffset = 0f + detachDirectionEstimator.reset() updateMagneticAndRoundableTargets(swipingRow, stackScrollLayout, sectionsManager) currentState = State.TARGETS_SET } else { @@ -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) @@ -154,6 +163,7 @@ constructor( } } State.DETACHED -> { + detachDirectionEstimator.recordTranslation(correctedTranslation) translateDetachedRow(correctedTranslation) } } @@ -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 @@ -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 @@ -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 @@ -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() @@ -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, @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 612c19fc6696..80775f4ee59f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -486,9 +486,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { } @Override - public boolean isMagneticViewDetached(View view) { + public boolean isMagneticViewDismissible(View view, float endVelocity) { if (view instanceof ExpandableNotificationRow row) { - return mMagneticNotificationRowManager.isMagneticRowSwipeDetached(row); + return mMagneticNotificationRowManager.isMagneticRowSwipedDismissible(row, + endVelocity); } else { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java index 5105e55b0a5c..d0096da8b269 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -255,13 +255,12 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc int menuSnapTarget = menuRow.getMenuSnapTarget(); boolean isNonFalseMenuRevealingGesture = isMenuRevealingGestureAwayFromMenu && !isFalseGesture(); - boolean isMagneticViewDetached = mCallback.isMagneticViewDetached(animView); if ((isNonDismissGestureTowardsMenu || isNonFalseMenuRevealingGesture) && menuSnapTarget != 0) { // Menu has not been snapped to previously and this is menu revealing gesture snapOpen(animView, menuSnapTarget, velocity); menuRow.onSnapOpen(); - } else if (isDismissGesture && (!gestureTowardsMenu || isMagneticViewDetached)) { + } else if (isDismissGesture && (!gestureTowardsMenu || isSwipeDismissible())) { dismiss(animView, velocity); menuRow.onDismiss(); } else { @@ -273,7 +272,6 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc private void handleSwipeFromOpenState(MotionEvent ev, View animView, float velocity, NotificationMenuRowPlugin menuRow) { boolean isDismissGesture = isDismissGesture(ev); - boolean isMagneticViewDetached = mCallback.isMagneticViewDetached(animView); final boolean withinSnapMenuThreshold = menuRow.isWithinSnapMenuThreshold(); @@ -282,7 +280,7 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc // Haven't moved enough to unsnap from the menu menuRow.onSnapOpen(); snapOpen(animView, menuRow.getMenuSnapTarget(), velocity); - } else if (isDismissGesture && (!menuRow.shouldSnapBack() || isMagneticViewDetached)) { + } else if (isDismissGesture && (!menuRow.shouldSnapBack() || isSwipeDismissible())) { // Only dismiss if we're not moving towards the menu dismiss(animView, velocity); menuRow.onDismiss(); |