Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7c41f9bb authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Modifiying the rules that determine magnetic dismissibility" into main

parents b552211d b9b6d74e
Loading
Loading
Loading
Loading
+45 −2
Original line number Diff line number Diff line
@@ -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 {
+2 −1
Original line number Diff line number Diff line
@@ -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();
@@ -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);

+8 −4
Original line number Diff line number Diff line
@@ -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.
+7 −4
Original line number Diff line number Diff line
@@ -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() {}
+95 −2
Original line number Diff line number Diff line
@@ -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

Loading