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

Commit 8c5a4918 authored by Lyn Han's avatar Lyn Han
Browse files

Fix HUN pinning

1) Fix IF we pin ----------

To pin HUNs, in StackScrollAlgorithm#updateHeadsUpStates, we need
mustStayOnScreen=True, but it is False
headsUpIsVisible=False, but it is True

How this happened:
Incoming HUNs have viewState.yTranslation=0 initially.

SSA#updateChild sets `headsUpIsVisible=True` based on
viewState.yTranslation=0 __before__ viewState.yTranslation is set
to mCurrentYPosition of AlgorithmState, marking the incoming HUN
as pinned and seen (when in fact it was scrolled above the screen)

Downstream in ExpandableNotificationRow,
setHeadsUpIsVisible sets mustStayOnScreen=False.
which skips pinning on the next iteration of
StackScrollAlgorithm#updateHeadsUpStates.


2) Fix WHERE we pin ----------

In S, we introduced fullscreen notification scrolling that covers quick quick settings, such that the qqs offset (visually, the bottom of the status bar when shade is open) is where notifications get clipped as they scroll up. Use this as the highest pin point instead of topPadding.


3) Implement pinned HUN CORNER ROUNDNESS animation ----------
   during expanded_qs <=> qqs/notifs transition.

When quick settings is expanded fullscreen, incoming HUNs are pinned to the bottom (y = top of nav bar - collapsed height) with ROUND bottom corners. When the user swipes up to show notifications again, if the pinned HUN is not the last notification in its section, animate its bottom corners from round to not_round.


4) Add tests and comments for the above, and for existing clampHunToTop implementation.


Fixes: 192566080
Test: StackScrollAlgorithmTest

Test: in test app, send delayed HUN with max priority
      => pin new/updated HUN to qqs offset when notifications are
         scrolled fullscreen; scroll to top, collapsed HUN transitions
         to full height as stack becomes unscrolled
      => show HUN first in unscrolled/slightly scrolled stack
      => pin HUN to bottom in expanded (fullscreen) quick settings,
         go back to qqs/notifications; corners unround smoothly
         if needed
      => pulse/lockscreen/normal HUN, swipe down from HUN to open
         shade; no regressions

Change-Id: Id51736be124f93ec524c2e74a5c879cd9489efc3
parent 9bf63618
Loading
Loading
Loading
Loading
+73 −24
Original line number Diff line number Diff line
@@ -24,8 +24,7 @@ import android.util.MathUtils;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.VisibleForTesting;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.SystemBarUtils;
import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.systemui.R;
@@ -65,6 +64,9 @@ public class StackScrollAlgorithm {
    private int mPinnedZTranslationExtra;
    private float mNotificationScrimPadding;
    private int mMarginBottom;
    private float mQuickQsOffsetHeight;
    private float mSmallCornerRadius;
    private float mLargeCornerRadius;

    public StackScrollAlgorithm(
            Context context,
@@ -74,10 +76,10 @@ public class StackScrollAlgorithm {
    }

    public void initView(Context context) {
        initConstants(context);
        updateResources(context);
    }

    private void initConstants(Context context) {
    private void updateResources(Context context) {
        Resources res = context.getResources();
        mPaddingBetweenElements = res.getDimensionPixelSize(
                R.dimen.notification_divider_height);
@@ -93,6 +95,9 @@ public class StackScrollAlgorithm {
                R.dimen.notification_section_divider_height_lockscreen);
        mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
        mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
        mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context);
        mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small);
        mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius);
    }

    /**
@@ -441,6 +446,15 @@ public class StackScrollAlgorithm {
        return false;
    }

    @VisibleForTesting
    void maybeUpdateHeadsUpIsVisible(ExpandableViewState viewState, boolean isShadeExpanded,
            boolean mustStayOnScreen, boolean topVisible, float viewEnd, float hunMax) {

        if (isShadeExpanded && mustStayOnScreen && topVisible) {
            viewState.headsUpIsVisible = viewEnd < hunMax;
        }
    }

    // TODO(b/172289889) polish shade open from HUN
    /**
     * Populates the {@link ExpandableViewState} for a single child.
@@ -474,14 +488,6 @@ public class StackScrollAlgorithm {
                    : ShadeInterpolation.getContentAlpha(expansion);
        }

        if (ambientState.isShadeExpanded() && view.mustStayOnScreen()
                && viewState.yTranslation >= 0) {
            // Even if we're not scrolled away we're in view and we're also not in the
            // shelf. We can relax the constraints and let us scroll off the top!
            float end = viewState.yTranslation + viewState.height + ambientState.getStackY();
            viewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
        }

        final float expansionFraction = getExpansionFractionWithoutShelf(
                algorithmState, ambientState);

@@ -497,8 +503,15 @@ public class StackScrollAlgorithm {
            algorithmState.mCurrentExpandedYPosition += gap;
        }

        // Must set viewState.yTranslation _before_ use.
        // Incoming views have yTranslation=0 by default.
        viewState.yTranslation = algorithmState.mCurrentYPosition;

        maybeUpdateHeadsUpIsVisible(viewState, ambientState.isShadeExpanded(),
                view.mustStayOnScreen(), /* topVisible */ viewState.yTranslation >= 0,
                /* viewEnd */ viewState.yTranslation + viewState.height + ambientState.getStackY(),
                /* hunMax */ ambientState.getMaxHeadsUpTranslation()
        );
        if (view instanceof FooterView) {
            final boolean shadeClosed = !ambientState.isShadeExpanded();
            final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
@@ -682,7 +695,8 @@ public class StackScrollAlgorithm {
                if (row.mustStayOnScreen() && !childState.headsUpIsVisible
                        && !row.showingPulsing()) {
                    // Ensure that the heads up is always visible even when scrolled off
                    clampHunToTop(ambientState, row, childState);
                    clampHunToTop(mQuickQsOffsetHeight, ambientState.getStackTranslation(),
                            row.getCollapsedHeight(), childState);
                    if (isTopEntry && row.isAboveShelf()) {
                        // the first hun can't get off screen.
                        clampHunToMaxTranslation(ambientState, row, childState);
@@ -719,27 +733,62 @@ public class StackScrollAlgorithm {
        }
    }

    private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
            ExpandableViewState childState) {
        float newTranslation = Math.max(ambientState.getTopPadding()
                + ambientState.getStackTranslation(), childState.yTranslation);
        childState.height = (int) Math.max(childState.height - (newTranslation
                - childState.yTranslation), row.getCollapsedHeight());
        childState.yTranslation = newTranslation;
    /**
     * When shade is open and we are scrolled to the bottom of notifications,
     * clamp incoming HUN in its collapsed form, right below qs offset.
     * Transition pinned collapsed HUN to full height when scrolling back up.
     */
    @VisibleForTesting
    void clampHunToTop(float quickQsOffsetHeight, float stackTranslation, float collapsedHeight,
            ExpandableViewState viewState) {

        final float newTranslation = Math.max(quickQsOffsetHeight + stackTranslation,
                viewState.yTranslation);

        // Transition from collapsed pinned state to fully expanded state
        // when the pinned HUN approaches its actual location (when scrolling back to top).
        final float distToRealY = newTranslation - viewState.yTranslation;
        viewState.height = (int) Math.max(viewState.height - distToRealY, collapsedHeight);
        viewState.yTranslation = newTranslation;
    }

    // Pin HUN to bottom of expanded QS
    // while the rest of notifications are scrolled offscreen.
    private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
            ExpandableViewState childState) {
        float newTranslation;
        float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
        float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
        final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
                + ambientState.getStackTranslation();
        maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
        float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
        newTranslation = Math.min(childState.yTranslation, bottomPosition);

        final float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
        final float newTranslation = Math.min(childState.yTranslation, bottomPosition);
        childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
                - newTranslation);
        childState.yTranslation = newTranslation;

        // Animate pinned HUN bottom corners to and from original roundness.
        final float originalCornerRadius =
                row.isLastInSection() ? 1f : (mSmallCornerRadius / mLargeCornerRadius);
        final float roundness = computeCornerRoundnessForPinnedHun(mHostView.getHeight(),
                ambientState.getStackY(), getMaxAllowedChildHeight(row), originalCornerRadius);
        row.setBottomRoundness(roundness, /* animate= */ false);
    }

    @VisibleForTesting
    float computeCornerRoundnessForPinnedHun(float hostViewHeight, float stackY,
            float viewMaxHeight, float originalCornerRadius) {

        // Compute y where corner roundness should be in its original unpinned state.
        // We use view max height because the pinned collapsed HUN expands to max height
        // when it becomes unpinned.
        final float originalRoundnessY = hostViewHeight - viewMaxHeight;

        final float distToOriginalRoundness = Math.max(0f, stackY - originalRoundnessY);
        final float progressToPinnedRoundness = Math.min(1f,
                distToOriginalRoundness / viewMaxHeight);

        return MathUtils.lerp(originalCornerRadius, 1f, progressToPinnedRoundness);
    }

    protected int getMaxAllowedChildHeight(View child) {
+175 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
@@ -164,4 +165,178 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
        stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
        assertFalse(expandableViewState.hidden)
    }

    @Test
    fun maybeUpdateHeadsUpIsVisible_endVisible_true() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.headsUpIsVisible = false

        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
                /* isShadeExpanded= */ true,
                /* mustStayOnScreen= */ true,
                /* isViewEndVisible= */ true,
                /* viewEnd= */ 0f,
                /* maxHunY= */ 10f)

        assertTrue(expandableViewState.headsUpIsVisible)
    }

    @Test
    fun maybeUpdateHeadsUpIsVisible_endHidden_false() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.headsUpIsVisible = true

        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
                /* isShadeExpanded= */ true,
                /* mustStayOnScreen= */ true,
                /* isViewEndVisible= */ true,
                /* viewEnd= */ 10f,
                /* maxHunY= */ 0f)

        assertFalse(expandableViewState.headsUpIsVisible)
    }

    @Test
    fun maybeUpdateHeadsUpIsVisible_shadeClosed_noUpdate() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.headsUpIsVisible = true

        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
                /* isShadeExpanded= */ false,
                /* mustStayOnScreen= */ true,
                /* isViewEndVisible= */ true,
                /* viewEnd= */ 10f,
                /* maxHunY= */ 1f)

        assertTrue(expandableViewState.headsUpIsVisible)
    }

    @Test
    fun maybeUpdateHeadsUpIsVisible_notHUN_noUpdate() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.headsUpIsVisible = true

        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
                /* isShadeExpanded= */ true,
                /* mustStayOnScreen= */ false,
                /* isViewEndVisible= */ true,
                /* viewEnd= */ 10f,
                /* maxHunY= */ 1f)

        assertTrue(expandableViewState.headsUpIsVisible)
    }

    @Test
    fun maybeUpdateHeadsUpIsVisible_topHidden_noUpdate() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.headsUpIsVisible = true

        stackScrollAlgorithm.maybeUpdateHeadsUpIsVisible(expandableViewState,
                /* isShadeExpanded= */ true,
                /* mustStayOnScreen= */ true,
                /* isViewEndVisible= */ false,
                /* viewEnd= */ 10f,
                /* maxHunY= */ 1f)

        assertTrue(expandableViewState.headsUpIsVisible)
    }

    @Test
    fun clampHunToTop_viewYGreaterThanQqs_viewYUnchanged() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.yTranslation = 50f

        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
                /* stackTranslation= */ 0f,
                /* collapsedHeight= */ 1f, expandableViewState)

        // qqs (10 + 0) < viewY (50)
        assertEquals(50f, expandableViewState.yTranslation)
    }

    @Test
    fun clampHunToTop_viewYLessThanQqs_viewYChanged() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.yTranslation = -10f

        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
                /* stackTranslation= */ 0f,
                /* collapsedHeight= */ 1f, expandableViewState)

        // qqs (10 + 0) > viewY (-10)
        assertEquals(10f, expandableViewState.yTranslation)
    }


    @Test
    fun clampHunToTop_viewYFarAboveVisibleStack_heightCollapsed() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.height = 20
        expandableViewState.yTranslation = -100f

        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
                /* stackTranslation= */ 0f,
                /* collapsedHeight= */ 10f, expandableViewState)

        // newTranslation = max(10, -100) = 10
        // distToRealY = 10 - (-100f) = 110
        // height = max(20 - 110, 10f)
        assertEquals(10, expandableViewState.height)
    }

    @Test
    fun clampHunToTop_viewYNearVisibleStack_heightTallerThanCollapsed() {
        val expandableViewState = ExpandableViewState()
        expandableViewState.height = 20
        expandableViewState.yTranslation = 5f

        stackScrollAlgorithm.clampHunToTop(/* quickQsOffsetHeight= */ 10f,
                /* stackTranslation= */ 0f,
                /* collapsedHeight= */ 10f, expandableViewState)

        // newTranslation = max(10, 5) = 10
        // distToRealY = 10 - 5 = 5
        // height = max(20 - 5, 10) = 15
        assertEquals(15, expandableViewState.height)
    }

    @Test
    fun computeCornerRoundnessForPinnedHun_stackBelowScreen_round() {
        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                /* hostViewHeight= */ 100f,
                /* stackY= */ 110f,
                /* viewMaxHeight= */ 20f,
                /* originalCornerRoundness= */ 0f)
        assertEquals(1f, currentRoundness)
    }

    @Test
    fun computeCornerRoundnessForPinnedHun_stackAboveScreenBelowPinPoint_halfRound() {
        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                /* hostViewHeight= */ 100f,
                /* stackY= */ 90f,
                /* viewMaxHeight= */ 20f,
                /* originalCornerRoundness= */ 0f)
        assertEquals(0.5f, currentRoundness)
    }

    @Test
    fun computeCornerRoundnessForPinnedHun_stackAbovePinPoint_notRound() {
        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                /* hostViewHeight= */ 100f,
                /* stackY= */ 0f,
                /* viewMaxHeight= */ 20f,
                /* originalCornerRoundness= */ 0f)
        assertEquals(0f, currentRoundness)
    }

    @Test
    fun computeCornerRoundnessForPinnedHun_originallyRoundAndStackAbovePinPoint_round() {
        val currentRoundness = stackScrollAlgorithm.computeCornerRoundnessForPinnedHun(
                /* hostViewHeight= */ 100f,
                /* stackY= */ 0f,
                /* viewMaxHeight= */ 20f,
                /* originalCornerRoundness= */ 1f)
        assertEquals(1f, currentRoundness)
    }
}