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

Commit fdff7368 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez Committed by Android (Google) Code Review
Browse files

Merge "Adding magnetic behavior for non-dismissibles and other ExpandableViews." into main

parents 5e3edd05 72d225c4
Loading
Loading
Loading
Loading
+103 −38
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack

import android.os.testableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.dynamicanimation.animation.SpringForce
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -46,13 +47,14 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
    private val childrenNumber = 5
    private val stackScrollLayout = mock<NotificationStackScrollLayout>()
    private val sectionsManager = mock<NotificationSectionsManager>()
    private val swipedMultiplier = 0.5f
    private val msdlPlayer = kosmos.fakeMSDLPlayer
    private var canRowBeDismissed = true

    private val underTest = kosmos.magneticNotificationRowManagerImpl

    private lateinit var notificationTestHelper: NotificationTestHelper
    private lateinit var children: NotificationChildrenContainer
    private lateinit var swipedRow: ExpandableNotificationRow

    @Before
    fun setUp() {
@@ -60,14 +62,15 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
        notificationTestHelper =
            NotificationTestHelper(mContext, mDependency, kosmos.testableLooper, featureFlags)
        children = notificationTestHelper.createGroup(childrenNumber).childrenContainer
        swipedRow = children.attachedChildren[childrenNumber / 2]
        configureMagneticRowListener(swipedRow)
    }

    @Test
    fun setMagneticAndRoundableTargets_onIdle_targetsGetSet() =
        kosmos.testScope.runTest {
            // WHEN the targets are set for a row
            val row = children.attachedChildren[childrenNumber / 2]
            setTargetsForRow(row)
            setTargets()

            // THEN the magnetic and roundable targets are defined and the state is TARGETS_SET
            assertThat(underTest.currentState).isEqualTo(State.TARGETS_SET)
@@ -79,11 +82,10 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
    fun setMagneticRowTranslation_whenTargetsAreSet_startsPulling() =
        kosmos.testScope.runTest {
            // GIVEN targets are set
            val row = children.attachedChildren[childrenNumber / 2]
            setTargetsForRow(row)
            setTargets()

            // WHEN setting a translation for the swiped row
            underTest.setMagneticRowTranslation(row, translation = 100f)
            underTest.setMagneticRowTranslation(swipedRow, translation = 100f)

            // THEN the state moves to PULLING
            assertThat(underTest.currentState).isEqualTo(State.PULLING)
@@ -107,8 +109,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
    fun setMagneticRowTranslation_whenRowIsNotSwiped_doesNotSetMagneticTranslation() =
        kosmos.testScope.runTest {
            // GIVEN that targets are set
            val row = children.attachedChildren[childrenNumber / 2]
            setTargetsForRow(row)
            setTargets()

            // WHEN setting a translation for a row that is not being swiped
            val differentRow = children.attachedChildren[childrenNumber / 2 - 1]
@@ -120,57 +121,98 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
        }

    @Test
    fun setMagneticRowTranslation_belowThreshold_whilePulling_setsMagneticTranslations() =
    fun setMagneticRowTranslation_whenDismissible_belowThreshold_whenPulling_setsTranslations() =
        kosmos.testScope.runTest {
            // GIVEN a threshold of 100 px
            val threshold = 100f
            underTest.setSwipeThresholdPx(threshold)

            // GIVEN that targets are set and the rows are being pulled
            val row = children.attachedChildren[childrenNumber / 2]
            setTargetsForRow(row)
            underTest.setMagneticRowTranslation(row, translation = 100f)
            setTargets()
            underTest.setMagneticRowTranslation(swipedRow, translation = 100f)

            // WHEN setting a translation that will fall below the threshold
            val translation = threshold / swipedMultiplier - 50f
            underTest.setMagneticRowTranslation(row, translation)
            val translation = threshold / underTest.swipedRowMultiplier - 50f
            underTest.setMagneticRowTranslation(swipedRow, translation)

            // THEN the targets continue to be pulled and translations are set
            assertThat(underTest.currentState).isEqualTo(State.PULLING)
            assertThat(row.translation).isEqualTo(swipedMultiplier * translation)
            assertThat(swipedRow.translation).isEqualTo(underTest.swipedRowMultiplier * translation)
        }

    @Test
    fun setMagneticRowTranslation_aboveThreshold_whilePulling_detachesMagneticTargets() =
    fun setMagneticRowTranslation_whenNotDismissible_belowThreshold_whenPulling_setsTranslations() =
        kosmos.testScope.runTest {
            // GIVEN a threshold of 100 px
            val threshold = 100f
            underTest.setSwipeThresholdPx(threshold)

            // GIVEN that targets are set and the rows are being pulled
            val row = children.attachedChildren[childrenNumber / 2]
            setTargetsForRow(row)
            underTest.setMagneticRowTranslation(row, translation = 100f)
            canRowBeDismissed = false
            setTargets()
            underTest.setMagneticRowTranslation(swipedRow, translation = 100f)

            // WHEN setting a translation that will fall below the threshold
            val translation = threshold / underTest.swipedRowMultiplier - 50f
            underTest.setMagneticRowTranslation(swipedRow, translation)

            // THEN the targets continue to be pulled and reduced translations are set
            val expectedTranslation = getReducedTranslation(translation)
            assertThat(underTest.currentState).isEqualTo(State.PULLING)
            assertThat(swipedRow.translation).isEqualTo(expectedTranslation)
        }

    @Test
    fun setMagneticRowTranslation_whenDismissible_aboveThreshold_whilePulling_detaches() =
        kosmos.testScope.runTest {
            // GIVEN a threshold of 100 px
            val threshold = 100f
            underTest.setSwipeThresholdPx(threshold)

            // GIVEN that targets are set and the rows are being pulled
            setTargets()
            underTest.setMagneticRowTranslation(swipedRow, translation = 100f)

            // WHEN setting a translation that will fall above the threshold
            val translation = threshold / swipedMultiplier + 50f
            underTest.setMagneticRowTranslation(row, translation)
            val translation = threshold / underTest.swipedRowMultiplier + 50f
            underTest.setMagneticRowTranslation(swipedRow, translation)

            // THEN the swiped view detaches and the correct detach haptics play
            assertThat(underTest.currentState).isEqualTo(State.DETACHED)
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWIPE_THRESHOLD_INDICATOR)
        }

    @Test
    fun setMagneticRowTranslation_whenNotDismissible_aboveThreshold_whilePulling_doesNotDetach() =
        kosmos.testScope.runTest {
            // GIVEN a threshold of 100 px
            val threshold = 100f
            underTest.setSwipeThresholdPx(threshold)

            // GIVEN that targets are set and the rows are being pulled
            canRowBeDismissed = false
            setTargets()
            underTest.setMagneticRowTranslation(swipedRow, translation = 100f)

            // WHEN setting a translation that will fall above the threshold
            val translation = threshold / underTest.swipedRowMultiplier + 50f
            underTest.setMagneticRowTranslation(swipedRow, translation)

            // THEN the swiped view does not detach and the reduced translation is set
            val expectedTranslation = getReducedTranslation(translation)
            assertThat(underTest.currentState).isEqualTo(State.PULLING)
            assertThat(swipedRow.translation).isEqualTo(expectedTranslation)
        }

    @Test
    fun setMagneticRowTranslation_whileDetached_setsTranslationAndStaysDetached() =
        kosmos.testScope.runTest {
            // GIVEN that the swiped view has been detached
            val row = children.attachedChildren[childrenNumber / 2]
            setDetachedState(row)
            setDetachedState()

            // WHEN setting a new translation
            val translation = 300f
            underTest.setMagneticRowTranslation(row, translation)
            underTest.setMagneticRowTranslation(swipedRow, translation)

            // THEN the swiped view continues to be detached
            assertThat(underTest.currentState).isEqualTo(State.DETACHED)
@@ -180,14 +222,13 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
    fun onMagneticInteractionEnd_whilePulling_goesToIdle() =
        kosmos.testScope.runTest {
            // GIVEN targets are set
            val row = children.attachedChildren[childrenNumber / 2]
            setTargetsForRow(row)
            setTargets()

            // WHEN setting a translation for the swiped row
            underTest.setMagneticRowTranslation(row, translation = 100f)
            underTest.setMagneticRowTranslation(swipedRow, translation = 100f)

            // WHEN the interaction ends on the row
            underTest.onMagneticInteractionEnd(row, velocity = null)
            underTest.onMagneticInteractionEnd(swipedRow, velocity = null)

            // THEN the state resets
            assertThat(underTest.currentState).isEqualTo(State.IDLE)
@@ -197,32 +238,56 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
    fun onMagneticInteractionEnd_whileDetached_goesToIdle() =
        kosmos.testScope.runTest {
            // GIVEN the swiped row is detached
            val row = children.attachedChildren[childrenNumber / 2]
            setDetachedState(row)
            setDetachedState()

            // WHEN the interaction ends on the row
            underTest.onMagneticInteractionEnd(row, velocity = null)
            underTest.onMagneticInteractionEnd(swipedRow, velocity = null)

            // THEN the state resets
            assertThat(underTest.currentState).isEqualTo(State.IDLE)
        }

    private fun setDetachedState(row: ExpandableNotificationRow) {
    private fun setDetachedState() {
        val threshold = 100f
        underTest.setSwipeThresholdPx(threshold)

        // Set the pulling state
        setTargetsForRow(row)
        underTest.setMagneticRowTranslation(row, translation = 100f)
        setTargets()
        underTest.setMagneticRowTranslation(swipedRow, translation = 100f)

        // Set a translation that will fall above the threshold
        val translation = threshold / swipedMultiplier + 50f
        underTest.setMagneticRowTranslation(row, translation)
        val translation = threshold / underTest.swipedRowMultiplier + 50f
        underTest.setMagneticRowTranslation(swipedRow, translation)

        assertThat(underTest.currentState).isEqualTo(State.DETACHED)
    }

    private fun setTargetsForRow(row: ExpandableNotificationRow) {
        underTest.setMagneticAndRoundableTargets(row, stackScrollLayout, sectionsManager)
    private fun setTargets() {
        underTest.setMagneticAndRoundableTargets(swipedRow, stackScrollLayout, sectionsManager)
    }

    private fun getReducedTranslation(originalTranslation: Float) =
        underTest.swipedRowMultiplier *
            originalTranslation *
            MagneticNotificationRowManagerImpl.MAGNETIC_REDUCTION

    private fun configureMagneticRowListener(row: ExpandableNotificationRow) {
        val listener =
            object : MagneticRowListener {
                override fun setMagneticTranslation(translation: Float) {
                    row.translation = translation
                }

                override fun triggerMagneticForce(
                    endTranslation: Float,
                    springForce: SpringForce,
                    startVelocity: Float,
                ) {}

                override fun cancelMagneticAnimations() {}

                override fun canRowBeDismissed(): Boolean = canRowBeDismissed
            }
        row.magneticRowListener = listener
    }
}
+26 −36
Original line number Diff line number Diff line
@@ -71,7 +71,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;

import com.android.app.animation.Interpolators;
import com.android.internal.annotations.VisibleForTesting;
@@ -361,39 +360,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        }
    };

    private final SpringAnimation mMagneticAnimator = new SpringAnimation(
            this, FloatPropertyCompat.createFloatPropertyCompat(TRANSLATE_CONTENT));

    private final MagneticRowListener mMagneticRowListener = new MagneticRowListener() {

    @Override
        public void setMagneticTranslation(float translation) {
            if (mMagneticAnimator.isRunning()) {
                mMagneticAnimator.animateToFinalPosition(translation);
            } else {
                setTranslation(translation);
            }
        }

        @Override
        public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce,
                float startVelocity) {
            cancelMagneticAnimations();
            mMagneticAnimator.setSpring(springForce);
            mMagneticAnimator.setStartVelocity(startVelocity);
            mMagneticAnimator.animateToFinalPosition(endTranslation);
        }

        @Override
        public void cancelMagneticAnimations() {
    protected void cancelTranslationAnimations() {
        cancelSnapBackAnimation();
        cancelTranslateAnimation();
            mMagneticAnimator.cancel();
    }
    };

    private void cancelSnapBackAnimation() {
        PhysicsAnimator<ExpandableNotificationRow> animator =
        PhysicsAnimator<ExpandableView> animator =
                PhysicsAnimator.getInstanceIfExists(this /* target */);
        if (animator != null) {
            animator.cancel();
@@ -2044,6 +2018,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
                new NotificationInlineImageCache());
        float radius = getResources().getDimension(R.dimen.notification_corner_radius_small);
        mSmallRoundness = radius / getMaxRadius();
        mMagneticAnimator = new SpringAnimation(
                this, FloatPropertyCompat.createFloatPropertyCompat(TRANSLATE_CONTENT));
        initDimens();
    }

@@ -3299,6 +3275,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        return mIsHeadsUp && mMustStayOnScreen || notificationsPinnedHunInShade() && isPinned();
    }

    /**
     * For the case of an {@link ExpandableNotificationRow}, the dismissibility of the row considers
     * the exposure of guts, the state of the  notification entry, and if the view itself is allowed
     * to be dismissed.
     */
    @Override
    public boolean canExpandableViewBeDismissed() {
        if (areGutsExposed() || !mEntry.hasFinishedInitialization()) {
            return false;
        }
        return canViewBeDismissed();
    }

    /**
     * @return Whether this view is allowed to be dismissed. Only valid for visible notifications as
     * otherwise some state might not be updated.
@@ -4067,6 +4056,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
    }

    @VisibleForTesting
    public void setMagneticRowListener(MagneticRowListener listener) {
        mMagneticRowListener = listener;
    }

    /**
     * Equivalent to View.OnLongClickListener with coordinates
     */
@@ -4317,8 +4311,4 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        }
        mLogger.logRemoveTransientRow(row.getEntry(), getEntry());
    }

    public MagneticRowListener getMagneticRowListener() {
        return mMagneticRowListener;
    }
}
+53 −0
Original line number Diff line number Diff line
@@ -33,6 +33,9 @@ import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;

import com.android.app.animation.Interpolators;
import com.android.systemui.Dumpable;
@@ -42,6 +45,7 @@ import com.android.systemui.statusbar.notification.Roundable;
import com.android.systemui.statusbar.notification.RoundableState;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
import com.android.systemui.statusbar.notification.stack.MagneticRowListener;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.util.Compile;
import com.android.systemui.util.DumpUtilsKt;
@@ -85,6 +89,55 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
    protected boolean mLastInSection;
    protected boolean mFirstInSection;

    protected SpringAnimation mMagneticAnimator = new SpringAnimation(
            this /* object */, DynamicAnimation.TRANSLATION_X);

    protected MagneticRowListener mMagneticRowListener = new MagneticRowListener() {

        @Override
        public void setMagneticTranslation(float translation) {
            if (mMagneticAnimator.isRunning()) {
                mMagneticAnimator.animateToFinalPosition(translation);
            } else {
                setTranslation(translation);
            }
        }

        @Override
        public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce,
                float startVelocity) {
            cancelMagneticAnimations();
            mMagneticAnimator.setSpring(springForce);
            mMagneticAnimator.setStartVelocity(startVelocity);
            mMagneticAnimator.animateToFinalPosition(endTranslation);
        }

        @Override
        public void cancelMagneticAnimations() {
            cancelTranslationAnimations();
            mMagneticAnimator.cancel();
        }

        @Override
        public boolean canRowBeDismissed() {
            return canExpandableViewBeDismissed();
        }
    };

    /**
     * @return true if the ExpandableView can be dismissed. False otherwise.
     */
    public boolean canExpandableViewBeDismissed() {
        return false;
    }

    /** Cancel any trailing animations on the translation of the view */
    protected void cancelTranslationAnimations(){}

    public MagneticRowListener getMagneticRowListener() {
        return mMagneticRowListener;
    }

    public ExpandableView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mViewState = createExpandableViewState();
+45 −18
Original line number Diff line number Diff line
@@ -57,7 +57,7 @@ constructor(
        SpringForce().setStiffness(SNAP_BACK_STIFFNESS).setDampingRatio(SNAP_BACK_DAMPING_RATIO)

    // Multiplier applied to the translation of a row while swiped
    private val swipedRowMultiplier =
    val swipedRowMultiplier =
        MAGNETIC_TRANSLATION_MULTIPLIERS[MAGNETIC_TRANSLATION_MULTIPLIERS.size / 2]

    override fun setSwipeThresholdPx(thresholdPx: Float) {
@@ -111,24 +111,22 @@ constructor(
    ): Boolean {
        if (!row.isSwipedTarget()) return false

        val canTargetBeDismissed =
            currentMagneticListeners.swipedListener()?.canRowBeDismissed() ?: false
        when (currentState) {
            State.IDLE -> {
                logger.logMagneticRowTranslationNotSet(currentState, row.entry)
                return false
            }
            State.TARGETS_SET -> {
                pullTargets(translation)
                pullTargets(translation, canTargetBeDismissed)
                currentState = State.PULLING
            }
            State.PULLING -> {
                val targetTranslation = swipedRowMultiplier * translation
                val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold
                if (crossedThreshold) {
                    snapNeighborsBack()
                    currentMagneticListeners.swipedListener()?.let { detach(it, translation) }
                    currentState = State.DETACHED
                if (canTargetBeDismissed) {
                    pullDismissibleRow(translation)
                } else {
                    pullTargets(translation)
                    pullTargets(translation, canSwipedBeDismissed = false)
                }
            }
            State.DETACHED -> {
@@ -139,23 +137,49 @@ constructor(
        return true
    }

    private fun pullTargets(translation: Float) {
    private fun pullDismissibleRow(translation: Float) {
        val targetTranslation = swipedRowMultiplier * translation
        val crossedThreshold = abs(targetTranslation) >= magneticDetachThreshold
        if (crossedThreshold) {
            snapNeighborsBack()
            currentMagneticListeners.swipedListener()?.let { detach(it, translation) }
            currentState = State.DETACHED
        } else {
            pullTargets(translation, canSwipedBeDismissed = true)
        }
    }

    private fun pullTargets(translation: Float, canSwipedBeDismissed: Boolean) {
        var targetTranslation: Float
        currentMagneticListeners.forEachIndexed { i, listener ->
            listener?.let {
                if (!canSwipedBeDismissed || !it.canRowBeDismissed()) {
                    // Use a reduced translation if the target swiped can't be dismissed or if the
                    // target itself can't be dismissed
                    targetTranslation =
                        MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation * MAGNETIC_REDUCTION
                } else {
                    targetTranslation = MAGNETIC_TRANSLATION_MULTIPLIERS[i] * translation
            listener?.setMagneticTranslation(targetTranslation)
                }
        playPullHaptics(mappedTranslation = swipedRowMultiplier * translation)
                it.setMagneticTranslation(targetTranslation)
            }
        }
        playPullHaptics(mappedTranslation = swipedRowMultiplier * translation, canSwipedBeDismissed)
    }

    private fun playPullHaptics(mappedTranslation: Float) {
    private fun playPullHaptics(mappedTranslation: Float, canSwipedBeDismissed: Boolean) {
        val normalizedTranslation = abs(mappedTranslation) / magneticDetachThreshold
        val vibrationScale =
            (normalizedTranslation * MAX_VIBRATION_SCALE).pow(VIBRATION_PERCEPTION_EXPONENT)
        val scaleFactor =
            if (canSwipedBeDismissed) {
                WEAK_VIBRATION_SCALE
            } else {
                STRONG_VIBRATION_SCALE
            }
        val vibrationScale = scaleFactor * normalizedTranslation
        msdlPlayer.playToken(
            MSDLToken.DRAG_INDICATOR_CONTINUOUS,
            InteractionProperties.DynamicVibrationScale(
                scale = vibrationScale,
                scale = vibrationScale.pow(VIBRATION_PERCEPTION_EXPONENT),
                vibrationAttributes = VIBRATION_ATTRIBUTES_PIPELINING,
            ),
        )
@@ -233,6 +257,8 @@ constructor(
         */
        private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f)

        const val MAGNETIC_REDUCTION = 0.65f

        /** Spring parameters for physics animators */
        private const val DETACH_STIFFNESS = 800f
        private const val DETACH_DAMPING_RATIO = 0.95f
@@ -244,7 +270,8 @@ constructor(
                .setUsage(VibrationAttributes.USAGE_TOUCH)
                .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
                .build()
        private const val MAX_VIBRATION_SCALE = 0.2f
        private const val VIBRATION_PERCEPTION_EXPONENT = 1 / 0.89f
        private const val WEAK_VIBRATION_SCALE = 0.2f
        private const val STRONG_VIBRATION_SCALE = 0.45f
    }
}
+3 −0
Original line number Diff line number Diff line
@@ -41,4 +41,7 @@ interface MagneticRowListener {

    /** Cancel any animations related to the magnetic interactions of the row */
    fun cancelMagneticAnimations()

    /** Can the row be dismissed. */
    fun canRowBeDismissed(): Boolean
}
Loading