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

Commit f06fc352 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez
Browse files

Implementing magnetic attachment using springs.

The ability to re-attach a magnetic notification to its neighbors is
being implemented. After being detached, crossing a new attachment
threshold reactivates the magnetic pulling. To avoid jump artificats,
the MagneticRowListeners decide if running animators must be cancelled
when setting a new translation if we wish to track the input gesture
directly in an eager fashion. This happens if the drag gestures are
larger than the set touch slop.

Test: MagneticNotificationRowManagerImplTest
Flag: com.android.systemui.magnetic_notification_swipes
Bug: 397418247
Change-Id: I04347232b4d7f386efc9c489184cf24354540f14
parent 1b11d6bc
Loading
Loading
Loading
Loading
+16 −2
Original line number Diff line number Diff line
@@ -311,6 +311,20 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
            assertThat(isDismissible).isTrue()
        }

    @Test
    fun setMagneticRowTranslation_whenDetached_belowAttachThreshold_reattaches() =
        kosmos.testScope.runTest {
            // GIVEN that the swiped view has been detached
            setDetachedState()

            // WHEN setting a new translation above the attach threshold
            val translation = 50f
            underTest.setMagneticRowTranslation(swipedRow, translation)

            // THEN the swiped view reattaches magnetically and the state becomes PULLING
            assertThat(underTest.currentState).isEqualTo(State.PULLING)
        }

    @After
    fun tearDown() {
        // We reset the manager so that all MagneticRowListener can cancel all animations
@@ -346,8 +360,8 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
    private fun MagneticRowListener.asTestableListener(rowIndex: Int): MagneticRowListener {
        val delegate = this
        return object : MagneticRowListener {
            override fun setMagneticTranslation(translation: Float) {
                delegate.setMagneticTranslation(translation)
            override fun setMagneticTranslation(translation: Float, trackEagerly: Boolean) {
                delegate.setMagneticTranslation(translation, trackEagerly)
            }

            override fun triggerMagneticForce(
+21 −4
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.row;
import static com.android.systemui.Flags.notificationColorUpdateLogger;
import static com.android.systemui.Flags.physicalNotificationMovement;

import static java.lang.Math.abs;

import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.Configuration;
@@ -29,6 +31,7 @@ import android.util.FloatProperty;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
@@ -110,15 +113,28 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
    protected SpringAnimation mMagneticAnimator = new SpringAnimation(
            this /* object */, DynamicAnimation.TRANSLATION_X);

    private int mTouchSlop;

    protected MagneticRowListener mMagneticRowListener = new MagneticRowListener() {

        @Override
        public void setMagneticTranslation(float translation) {
            if (mMagneticAnimator.isRunning()) {
        public void setMagneticTranslation(float translation, boolean trackEagerly) {
            if (!mMagneticAnimator.isRunning()) {
                setTranslation(translation);
                return;
            }

            if (trackEagerly) {
                float delta = abs(getTranslation() - translation);
                if (delta > mTouchSlop) {
                    mMagneticAnimator.animateToFinalPosition(translation);
                } else {
                    mMagneticAnimator.cancel();
                    setTranslation(translation);
                }
            } else {
                mMagneticAnimator.animateToFinalPosition(translation);
            }
        }

        @Override
@@ -183,6 +199,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
    private void initDimens() {
        mContentShift = getResources().getDimensionPixelSize(
                R.dimen.shelf_transform_content_shift);
        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    }

    @Override
+3 −0
Original line number Diff line number Diff line
@@ -107,6 +107,9 @@ interface MagneticNotificationRowManager {
        /** Detaching threshold in dp */
        const val MAGNETIC_DETACH_THRESHOLD_DP = 56

        /** Re-attaching threshold in dp */
        const val MAGNETIC_ATTACH_THRESHOLD_DP = 40

        /* An empty implementation of a manager */
        @JvmStatic
        val Empty: MagneticNotificationRowManager
+43 −2
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ constructor(
        private set

    private var magneticDetachThreshold = Float.POSITIVE_INFINITY
    private var magneticAttachThreshold = 0f

    // Has the roundable target been set for the magnetic view that is being swiped.
    val isSwipedViewRoundableSet: Boolean
@@ -57,6 +58,8 @@ constructor(
        SpringForce().setStiffness(DETACH_STIFFNESS).setDampingRatio(DETACH_DAMPING_RATIO)
    private val snapForce =
        SpringForce().setStiffness(SNAP_BACK_STIFFNESS).setDampingRatio(SNAP_BACK_DAMPING_RATIO)
    private val attachForce =
        SpringForce().setStiffness(ATTACH_STIFFNESS).setDampingRatio(ATTACH_DAMPING_RATIO)

    // Multiplier applied to the translation of a row while swiped
    val swipedRowMultiplier =
@@ -65,6 +68,8 @@ constructor(
    override fun onDensityChange(density: Float) {
        magneticDetachThreshold =
            density * MagneticNotificationRowManager.MAGNETIC_DETACH_THRESHOLD_DP
        magneticAttachThreshold =
            density * MagneticNotificationRowManager.MAGNETIC_ATTACH_THRESHOLD_DP
    }

    override fun setMagneticAndRoundableTargets(
@@ -140,8 +145,7 @@ constructor(
                }
            }
            State.DETACHED -> {
                val swiped = currentMagneticListeners.swipedListener()
                swiped?.setMagneticTranslation(translation)
                translateDetachedRow(translation)
            }
        }
        return true
@@ -233,6 +237,41 @@ constructor(
        )
    }

    private fun translateDetachedRow(translation: Float) {
        val targetTranslation = swipedRowMultiplier * translation
        val crossedThreshold = abs(targetTranslation) <= magneticAttachThreshold
        if (crossedThreshold) {
            attachNeighbors(translation)
            updateRoundness(translation)
            currentMagneticListeners.swipedListener()?.let { attach(it, translation) }
            currentState = State.PULLING
        } else {
            val swiped = currentMagneticListeners.swipedListener()
            swiped?.setMagneticTranslation(translation, trackEagerly = false)
        }
    }

    private fun attachNeighbors(translation: Float) {
        currentMagneticListeners.forEachIndexed { i, target ->
            target?.let {
                if (i != currentMagneticListeners.size / 2) {
                    val multiplier = MAGNETIC_TRANSLATION_MULTIPLIERS[i]
                    target.cancelMagneticAnimations()
                    target.triggerMagneticForce(
                        endTranslation = translation * multiplier,
                        attachForce,
                    )
                }
            }
        }
    }

    private fun attach(listener: MagneticRowListener, toPosition: Float) {
        listener.cancelMagneticAnimations()
        listener.triggerMagneticForce(toPosition, attachForce)
        msdlPlayer.playToken(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
    }

    override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) {
        if (row.isSwipedTarget()) {
            when (currentState) {
@@ -304,6 +343,8 @@ constructor(
        private const val DETACH_DAMPING_RATIO = 0.95f
        private const val SNAP_BACK_STIFFNESS = 550f
        private const val SNAP_BACK_DAMPING_RATIO = 0.6f
        private const val ATTACH_STIFFNESS = 800f
        private const val ATTACH_DAMPING_RATIO = 0.95f

        // Maximum value of corner roundness that gets applied during the pre-detach dragging
        private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f
+15 −2
Original line number Diff line number Diff line
@@ -21,8 +21,21 @@ import androidx.dynamicanimation.animation.SpringForce
/** A listener that responds to magnetic forces applied to an [ExpandableNotificationRow] */
interface MagneticRowListener {

    /** Set a translation due to a magnetic attachment. */
    fun setMagneticTranslation(translation: Float)
    /**
     * Set a translation due to a magnetic attachment.
     *
     * The request to set a View's translation may occur while a magnetic animation is running. In
     * such a case, the [trackEagerly] argument will determine if we decide to eagerly track the
     * incoming translation or not. If true, the ongoing animation will update its target position
     * and continue to animate only if the incoming translation produces a delta higher than a touch
     * slop threshold. Otherwise, the animation will be cancelled and the View translation will be
     * set directly. If [trackEagerly] is false, we respect the animation and only update the
     * animated target.
     *
     * @param[translation] Incoming gesture translation.
     * @param[trackEagerly] Whether we eagerly track the incoming translation directly or not.
     */
    fun setMagneticTranslation(translation: Float, trackEagerly: Boolean = true)

    /**
     * Trigger the magnetic behavior when the row detaches or snaps back from its magnetic