Merge "Fix HUN pinning" into tm-qpr-dev

This commit is contained in:
Lyn Han
2022-07-01 16:15:53 +00:00
committed by Android (Google) Code Review
2 changed files with 248 additions and 24 deletions

View File

@@ -24,8 +24,7 @@ import android.util.MathUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.SystemBarUtils; import com.android.internal.policy.SystemBarUtils;
import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.keyguard.BouncerPanelExpansionCalculator;
import com.android.systemui.R; import com.android.systemui.R;
@@ -65,6 +64,9 @@ public class StackScrollAlgorithm {
private int mPinnedZTranslationExtra; private int mPinnedZTranslationExtra;
private float mNotificationScrimPadding; private float mNotificationScrimPadding;
private int mMarginBottom; private int mMarginBottom;
private float mQuickQsOffsetHeight;
private float mSmallCornerRadius;
private float mLargeCornerRadius;
public StackScrollAlgorithm( public StackScrollAlgorithm(
Context context, Context context,
@@ -74,10 +76,10 @@ public class StackScrollAlgorithm {
} }
public void initView(Context context) { public void initView(Context context) {
initConstants(context); updateResources(context);
} }
private void initConstants(Context context) { private void updateResources(Context context) {
Resources res = context.getResources(); Resources res = context.getResources();
mPaddingBetweenElements = res.getDimensionPixelSize( mPaddingBetweenElements = res.getDimensionPixelSize(
R.dimen.notification_divider_height); R.dimen.notification_divider_height);
@@ -93,6 +95,9 @@ public class StackScrollAlgorithm {
R.dimen.notification_section_divider_height_lockscreen); R.dimen.notification_section_divider_height_lockscreen);
mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings); mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom); 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; 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 // TODO(b/172289889) polish shade open from HUN
/** /**
* Populates the {@link ExpandableViewState} for a single child. * Populates the {@link ExpandableViewState} for a single child.
@@ -474,14 +488,6 @@ public class StackScrollAlgorithm {
: ShadeInterpolation.getContentAlpha(expansion); : 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( final float expansionFraction = getExpansionFractionWithoutShelf(
algorithmState, ambientState); algorithmState, ambientState);
@@ -497,8 +503,15 @@ public class StackScrollAlgorithm {
algorithmState.mCurrentExpandedYPosition += gap; algorithmState.mCurrentExpandedYPosition += gap;
} }
// Must set viewState.yTranslation _before_ use.
// Incoming views have yTranslation=0 by default.
viewState.yTranslation = algorithmState.mCurrentYPosition; 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) { if (view instanceof FooterView) {
final boolean shadeClosed = !ambientState.isShadeExpanded(); final boolean shadeClosed = !ambientState.isShadeExpanded();
final boolean isShelfShowing = algorithmState.firstViewInShelf != null; final boolean isShelfShowing = algorithmState.firstViewInShelf != null;
@@ -682,7 +695,8 @@ public class StackScrollAlgorithm {
if (row.mustStayOnScreen() && !childState.headsUpIsVisible if (row.mustStayOnScreen() && !childState.headsUpIsVisible
&& !row.showingPulsing()) { && !row.showingPulsing()) {
// Ensure that the heads up is always visible even when scrolled off // 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()) { if (isTopEntry && row.isAboveShelf()) {
// the first hun can't get off screen. // the first hun can't get off screen.
clampHunToMaxTranslation(ambientState, row, childState); clampHunToMaxTranslation(ambientState, row, childState);
@@ -719,27 +733,62 @@ public class StackScrollAlgorithm {
} }
} }
private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, /**
ExpandableViewState childState) { * When shade is open and we are scrolled to the bottom of notifications,
float newTranslation = Math.max(ambientState.getTopPadding() * clamp incoming HUN in its collapsed form, right below qs offset.
+ ambientState.getStackTranslation(), childState.yTranslation); * Transition pinned collapsed HUN to full height when scrolling back up.
childState.height = (int) Math.max(childState.height - (newTranslation */
- childState.yTranslation), row.getCollapsedHeight()); @VisibleForTesting
childState.yTranslation = newTranslation; 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, private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
ExpandableViewState childState) { ExpandableViewState childState) {
float newTranslation;
float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() final float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
+ ambientState.getStackTranslation(); + ambientState.getStackTranslation();
maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); 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 childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
- newTranslation); - newTranslation);
childState.yTranslation = 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) { protected int getMaxAllowedChildHeight(View child) {

View File

@@ -15,6 +15,7 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertFalse import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue import junit.framework.Assert.assertTrue
import junit.framework.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
@@ -164,4 +165,178 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart) stackScrollAlgorithm.updateViewWithShelf(expandableView, expandableViewState, shelfStart)
assertFalse(expandableViewState.hidden) 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)
}
} }