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

Commit 7df96aa9 authored by Lyn Han's avatar Lyn Han Committed by Automerger Merge Worker
Browse files

Merge "Fix HUN pinning" into tm-qpr-dev am: bc1144e6

parents caff4c4c bc1144e6
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)
    }
}