Short shelf on lockscreen that animates width with transition to shade

NotificationIconContainer provides the shortest shelf width based on
3 icons + 1 overflow dot.

Propagate fraction-to-shade from LockscreenShadeTransitionController
=> NSSLC => NSSL => NotificationShelf, which uses it to calculates shelf width

NSSL requests children update,
then NotificationShelf propagates shelf width
=> NotificationBackgroundView for corner roundness positioning
=> NotificationIconContainer to limit icons to new width

NotificationShelf ensures that only the visible part of the shelf is tappable.

Bug: 213480466
Test: NotificationShelfTest
Test: swipe down on lockscreen notifications, let go
	=> short shelf expands, retracts width
Test: swipe down on lockscreen notifications to go to unlocked shade
	=> short shelf expands to full width
Test: expand notification on lockscreen
	=> short shelf expands to full width as shade opens
Test: tap hidden/visible part of shelf
	=> only visible part of shelf launches full shade
Test: the above in LTR/RTL

Change-Id: I1d3f2a9077e6058d4459bb11602601c035b8043a
This commit is contained in:
Lyn
2022-01-19 10:46:28 -06:00
parent 9aaf48a2c4
commit 9a52b474d6
9 changed files with 358 additions and 33 deletions

View File

@@ -337,11 +337,11 @@ class LockscreenShadeTransitionController @Inject constructor(
if (field != value || forceApplyAmount) {
field = value
if (!nsslController.isInLockedDownShade() || field == 0f || forceApplyAmount) {
nsslController.setTransitionToFullShadeAmount(field)
qSDragProgress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance)
nsslController.setTransitionToFullShadeAmount(field, qSDragProgress)
qS.setTransitionToFullShadeAmount(field, qSDragProgress)
notificationPanelController.setTransitionToFullShadeAmount(field,
false /* animate */, 0 /* delay */)
qSDragProgress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance)
qS.setTransitionToFullShadeAmount(field, qSDragProgress)
// TODO: appear media also in split shade
val mediaAmount = if (useSplitShade) 0f else field
mediaHierarchyManager.setTransitionToFullShadeAmount(mediaAmount)

View File

@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
import static com.android.systemui.statusbar.phone.NotificationIconContainer.MAX_ICONS_ON_LOCKSCREEN;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -32,6 +34,7 @@ import android.view.animation.PathInterpolator;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.ShadeInterpolation;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -45,6 +48,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
import com.android.systemui.statusbar.notification.stack.ViewState;
import com.android.systemui.statusbar.phone.NotificationIconContainer;
import com.android.systemui.util.Utils;
/**
* A notification shelf view that is placed inside the notification scroller. It manages the
@@ -81,6 +85,11 @@ public class NotificationShelf extends ActivatableNotificationView implements
private int mIndexOfFirstViewInShelf = -1;
private float mCornerAnimationDistance;
private NotificationShelfController mController;
private int mActualWidth = -1;
private boolean mUseSplitShade;
/** Fraction of lockscreen to shade animation (on lockscreen swipe down). */
private float mFractionToShade;
public NotificationShelf(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -122,13 +131,16 @@ public class NotificationShelf extends ActivatableNotificationView implements
layoutParams.height = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
setLayoutParams(layoutParams);
int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
final int padding = res.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding);
mShelfIcons.setPadding(padding, 0, padding, 0);
mScrollFastThreshold = res.getDimensionPixelOffset(R.dimen.scroll_fast_threshold);
mShowNotificationShelf = res.getBoolean(R.bool.config_showNotificationShelf);
mCornerAnimationDistance = res.getDimensionPixelSize(
R.dimen.notification_corner_animation_distance);
// TODO(b/213480466) enable short shelf on split shade
mUseSplitShade = Utils.shouldUseSplitNotificationShade(mContext.getResources());
mShelfIcons.setInNotificationIconShelf(true);
if (!mShowNotificationShelf) {
setVisibility(GONE);
@@ -203,6 +215,10 @@ public class NotificationShelf extends ActivatableNotificationView implements
final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight();
viewState.yTranslation = stackEnd - viewState.height;
final int shortestWidth = mShelfIcons.calculateWidthFor(MAX_ICONS_ON_LOCKSCREEN);
final float fraction = Interpolators.STANDARD.getInterpolation(mFractionToShade);
updateStateWidth(viewState, fraction, shortestWidth);
} else {
viewState.hidden = true;
viewState.location = ExpandableViewState.LOCATION_GONE;
@@ -210,6 +226,77 @@ public class NotificationShelf extends ActivatableNotificationView implements
}
}
/**
* @param shelfState View state for NotificationShelf
* @param fraction Fraction of lockscreen to shade transition
* @param shortestWidth Shortest width to use for lockscreen shelf
*/
@VisibleForTesting
public void updateStateWidth(ShelfState shelfState, float fraction, int shortestWidth) {
shelfState.actualWidth = !mUseSplitShade && mAmbientState.isOnKeyguard()
? (int) MathUtils.lerp(shortestWidth, getWidth(), fraction)
: getWidth();
}
/**
* @param fractionToShade Fraction of lockscreen to shade transition
*/
public void setFractionToShade(float fractionToShade) {
mFractionToShade = fractionToShade;
}
/**
* @return Actual width of shelf, accounting for possible ongoing width animation
*/
public int getActualWidth() {
return mActualWidth > -1 ? mActualWidth : getWidth();
}
/**
* @param localX Click x from left of screen
* @param slop Margin of error within which we count x for valid click
* @param left Left of shelf, from left of screen
* @param right Right of shelf, from left of screen
* @return Whether click x was in view
*/
@VisibleForTesting
public boolean isXInView(float localX, float slop, float left, float right) {
return (left - slop) <= localX && localX < (right + slop);
}
/**
* @param localY Click y from top of shelf
* @param slop Margin of error within which we count y for valid click
* @param top Top of shelf
* @param bottom Height of shelf
* @return Whether click y was in view
*/
@VisibleForTesting
public boolean isYInView(float localY, float slop, float top, float bottom) {
return (top - slop) <= localY && localY < (bottom + slop);
}
/**
* @param localX Click x
* @param localY Click y
* @param slop Margin of error for valid click
* @return Whether this click was on the visible (non-clipped) part of the shelf
*/
@Override
public boolean pointInView(float localX, float localY, float slop) {
final float containerWidth = getWidth();
final float shelfWidth = getActualWidth();
final float left = isLayoutRtl() ? containerWidth - shelfWidth : 0;
final float right = isLayoutRtl() ? containerWidth : shelfWidth;
final float top = mClipTopAmount;
final float bottom = getActualHeight();
return isXInView(localX, slop, left, right)
&& isYInView(localY, slop, top, bottom);
}
/**
* Update the shelf appearance based on the other notifications around it. This transforms
* the icons from the notification area into the shelf.
@@ -732,11 +819,15 @@ public class NotificationShelf extends ActivatableNotificationView implements
// we always want to clip to our sides, such that nothing can draw outside of these bounds
int height = getResources().getDisplayMetrics().heightPixels;
mClipRect.set(0, -height, getWidth(), height);
mShelfIcons.setClipBounds(mClipRect);
if (mShelfIcons != null) {
mShelfIcons.setClipBounds(mClipRect);
}
}
private void updateRelativeOffset() {
mCollapsedIcons.getLocationOnScreen(mTmp);
if (mCollapsedIcons != null) {
mCollapsedIcons.getLocationOnScreen(mTmp);
}
getLocationOnScreen(mTmp);
}
@@ -831,9 +922,20 @@ public class NotificationShelf extends ActivatableNotificationView implements
mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf);
}
private class ShelfState extends ExpandableViewState {
public class ShelfState extends ExpandableViewState {
private boolean hasItemsInStableShelf;
private ExpandableView firstViewInShelf;
public int actualWidth = -1;
private void updateShelfWidth(View view) {
if (actualWidth < 0) {
return;
}
mActualWidth = actualWidth;
ActivatableNotificationView anv = (ActivatableNotificationView) view;
anv.getBackgroundNormal().setActualWidth(actualWidth);
mShelfIcons.setActualLayoutWidth(actualWidth);
}
@Override
public void applyToView(View view) {
@@ -846,19 +948,21 @@ public class NotificationShelf extends ActivatableNotificationView implements
updateAppearance();
setHasItemsInStableShelf(hasItemsInStableShelf);
mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
updateShelfWidth(view);
}
@Override
public void animateTo(View child, AnimationProperties properties) {
public void animateTo(View view, AnimationProperties properties) {
if (!mShowNotificationShelf) {
return;
}
super.animateTo(child, properties);
super.animateTo(view, properties);
setIndexOfFirstViewInShelf(firstViewInShelf);
updateAppearance();
setHasItemsInStableShelf(hasItemsInStableShelf);
mShelfIcons.setAnimationsEnabled(mAnimationsEnabled);
updateShelfWidth(view);
}
}
}

View File

@@ -162,6 +162,13 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
updateBackgroundTint();
}
/**
* @return The background of this view.
*/
public NotificationBackgroundView getBackgroundNormal() {
return mBackgroundNormal;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();

View File

@@ -29,6 +29,7 @@ import android.view.View;
import com.android.internal.util.ArrayUtils;
import com.android.systemui.R;
import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.notification.ExpandAnimationParameters;
/**
@@ -39,15 +40,17 @@ public class NotificationBackgroundView extends View {
private final boolean mDontModifyCorners;
private Drawable mBackground;
private int mClipTopAmount;
private int mActualHeight;
private int mClipBottomAmount;
private int mTintColor;
private final float[] mCornerRadii = new float[8];
private boolean mBottomIsRounded;
private int mBackgroundTop;
private boolean mBottomAmountClips = true;
private int mActualHeight = -1;
private int mActualWidth = -1;
private boolean mExpandAnimationRunning;
private float mActualWidth;
private int mExpandAnimationWidth = -1;
private int mExpandAnimationHeight = -1;
private int mDrawableAlpha = 255;
private boolean mIsPressedAllowed;
@@ -59,11 +62,12 @@ public class NotificationBackgroundView extends View {
@Override
protected void onDraw(Canvas canvas) {
if (mClipTopAmount + mClipBottomAmount < mActualHeight - mBackgroundTop
if (mClipTopAmount + mClipBottomAmount < getActualHeight() - mBackgroundTop
|| mExpandAnimationRunning) {
canvas.save();
if (!mExpandAnimationRunning) {
canvas.clipRect(0, mClipTopAmount, getWidth(), mActualHeight - mClipBottomAmount);
canvas.clipRect(0, mClipTopAmount, getWidth(),
getActualHeight() - mClipBottomAmount);
}
draw(canvas, mBackground);
canvas.restore();
@@ -73,17 +77,23 @@ public class NotificationBackgroundView extends View {
private void draw(Canvas canvas, Drawable drawable) {
if (drawable != null) {
int top = mBackgroundTop;
int bottom = mActualHeight;
int bottom = getActualHeight();
if (mBottomIsRounded
&& mBottomAmountClips
&& !mExpandAnimationRunning) {
bottom -= mClipBottomAmount;
}
int left = 0;
int right = getWidth();
final boolean isRtl = isLayoutRtl();
final int width = getWidth();
final int actualWidth = getActualWidth();
int left = isRtl ? width - actualWidth : 0;
int right = isRtl ? width : actualWidth;
if (mExpandAnimationRunning) {
left = (int) ((getWidth() - mActualWidth) / 2.0f);
right = (int) (left + mActualWidth);
// Horizontally center this background view inside of the container
left = (int) ((width - actualWidth) / 2.0f);
right = (int) (left + actualWidth);
}
drawable.setBounds(left, top, right, bottom);
drawable.draw(canvas);
@@ -152,8 +162,26 @@ public class NotificationBackgroundView extends View {
invalidate();
}
public int getActualHeight() {
return mActualHeight;
private int getActualHeight() {
if (mExpandAnimationRunning && mExpandAnimationHeight > -1) {
return mExpandAnimationHeight;
} else if (mActualHeight > -1) {
return mActualHeight;
}
return getHeight();
}
public void setActualWidth(int actualWidth) {
mActualWidth = actualWidth;
}
private int getActualWidth() {
if (mExpandAnimationRunning && mExpandAnimationWidth > -1) {
return mExpandAnimationWidth;
} else if (mActualWidth > -1) {
return mActualWidth;
}
return getWidth();
}
public void setClipTopAmount(int clipTopAmount) {
@@ -241,9 +269,9 @@ public class NotificationBackgroundView extends View {
}
/** Set the current expand animation size. */
public void setExpandAnimationSize(int actualWidth, int actualHeight) {
mActualHeight = actualHeight;
mActualWidth = actualWidth;
public void setExpandAnimationSize(int width, int height) {
mExpandAnimationHeight = width;
mExpandAnimationWidth = height;
invalidate();
}

View File

@@ -5442,6 +5442,17 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
requestChildrenUpdate();
}
/**
* @param fraction Fraction of the lockscreen to shade transition. 0f for all other states.
* Once the lockscreen to shade transition completes and the shade is 100% open
* LockscreenShadeTransitionController resets fraction to 0
* where it remains until the next lockscreen-to-shade transition.
*/
public void setFractionToShade(float fraction) {
mShelf.setFractionToShade(fraction);
requestChildrenUpdate();
}
/**
* Set a listener to when scrolling changes.
*/

View File

@@ -1511,10 +1511,18 @@ public class NotificationStackScrollLayoutController {
}
/**
* Set the amount of pixels we have currently dragged down if we're transitioning to the full
* shade. 0.0f means we're not transitioning yet.
* @param amount The amount of pixels we have currently dragged down
* for the lockscreen to shade transition. 0f for all other states.
* @param fraction The fraction of lockscreen to shade transition.
* 0f for all other states.
*
* Once the lockscreen to shade transition completes and the shade is 100% open,
* LockscreenShadeTransitionController resets amount and fraction to 0, where they remain
* until the next lockscreen-to-shade transition.
*/
public void setTransitionToFullShadeAmount(float amount) {
public void setTransitionToFullShadeAmount(float amount, float fraction) {
mView.setFractionToShade(fraction);
float extraTopInset = 0.0f;
if (mStatusBarStateController.getState() == KEYGUARD) {
float overallProgress = MathUtils.saturate(amount / mView.getHeight());

View File

@@ -135,7 +135,8 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
}
}.setDuration(CONTENT_FADE_DURATION);
private static final int MAX_VISIBLE_ICONS_ON_LOCK = 5;
private static final int MAX_ICONS_ON_AOD = 5;
public static final int MAX_ICONS_ON_LOCKSCREEN = 3;
public static final int MAX_STATIC_ICONS = 4;
private static final int MAX_DOTS = 1;
@@ -385,6 +386,19 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
}
}
/**
* @return Width of shelf for the given number of icons and overflow dot
*/
public int calculateWidthFor(int numMaxIcons) {
if (getChildCount() == 0) {
return 0;
}
return (int) (getActualPaddingStart()
+ numMaxIcons * mIconSize
+ mOverflowWidth
+ getActualPaddingEnd());
}
/**
* Calculate the horizontal translations for each notification based on how much the icons
* are inserted into the notification container.
@@ -394,7 +408,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
float translationX = getActualPaddingStart();
int firstOverflowIndex = -1;
int childCount = getChildCount();
int maxVisibleIcons = mOnLockScreen ? MAX_VISIBLE_ICONS_ON_LOCK :
int maxVisibleIcons = mOnLockScreen ? MAX_ICONS_ON_AOD :
mIsStaticLayout ? MAX_STATIC_ICONS : childCount;
float layoutEnd = getLayoutEnd();
float overflowStart = getMaxOverflowStart();
@@ -414,7 +428,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
}
boolean forceOverflow = mSpeedBumpIndex != -1 && i >= mSpeedBumpIndex
&& iconState.iconAppearAmount > 0.0f || i >= maxVisibleIcons;
boolean noOverflowAfter = i == childCount - 1;
boolean isLastChild = i == childCount - 1;
float drawingScale = mOnLockScreen && view instanceof StatusBarIconView
? ((StatusBarIconView) view).getIconScaleIncreased()
: 1f;
@@ -423,10 +437,10 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout {
: StatusBarIconView.STATE_ICON;
boolean isOverflowing =
(translationX > (noOverflowAfter ? layoutEnd - mIconSize
(translationX > (isLastChild ? layoutEnd - mIconSize
: overflowStart - mIconSize));
if (firstOverflowIndex == -1 && (forceOverflow || isOverflowing)) {
firstOverflowIndex = noOverflowAfter && !forceOverflow ? i - 1 : i;
firstOverflowIndex = isLastChild && !forceOverflow ? i - 1 : i;
mVisualOverflowStart = layoutEnd - mOverflowWidth;
if (forceOverflow || mIsStaticLayout) {
mVisualOverflowStart = Math.min(translationX, mVisualOverflowStart);

View File

@@ -227,7 +227,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() {
fun testDragDownAmountDoesntCallOutInLockedDownShade() {
whenever(nsslController.isInLockedDownShade).thenReturn(true)
transitionController.dragDownAmount = 10f
verify(nsslController, never()).setTransitionToFullShadeAmount(anyFloat())
verify(nsslController, never()).setTransitionToFullShadeAmount(anyFloat(), anyFloat())
verify(mediaHierarchyManager, never()).setTransitionToFullShadeAmount(anyFloat())
verify(scrimController, never()).setTransitionToFullShadeProgress(anyFloat())
verify(notificationPanelController, never()).setTransitionToFullShadeAmount(anyFloat(),
@@ -238,7 +238,7 @@ class LockscreenShadeTransitionControllerTest : SysuiTestCase() {
@Test
fun testDragDownAmountCallsOut() {
transitionController.dragDownAmount = 10f
verify(nsslController).setTransitionToFullShadeAmount(anyFloat())
verify(nsslController).setTransitionToFullShadeAmount(anyFloat(), anyFloat())
verify(mediaHierarchyManager).setTransitionToFullShadeAmount(anyFloat())
verify(scrimController).setTransitionToFullShadeProgress(anyFloat())
verify(notificationPanelController).setTransitionToFullShadeAmount(anyFloat(),

View File

@@ -0,0 +1,153 @@
package com.android.systemui.statusbar.notification.stack
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.NotificationShelf
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.*
import org.mockito.Mockito.`when` as whenever
/**
* Tests for {@link NotificationShelf}.
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class NotificationShelfTest : SysuiTestCase() {
private val shelf = NotificationShelf(context, /* attrs */ null)
private val shelfState = shelf.viewState as NotificationShelf.ShelfState
private val ambientState = mock(AmbientState::class.java)
@Before
fun setUp() {
shelf.bind(ambientState, /* hostLayoutController */ null)
shelf.layout(/* left */ 0, /* top */ 0, /* right */ 30, /* bottom */5)
}
@Test
fun testShadeWidth_BasedOnFractionToShade() {
setFractionToShade(0f)
setOnLockscreen(true)
shelf.updateStateWidth(shelfState, /* fraction */ 0f, /* shortestWidth */ 10)
assertTrue(shelfState.actualWidth == 10)
shelf.updateStateWidth(shelfState, /* fraction */ 0.5f, /* shortestWidth */ 10)
assertTrue(shelfState.actualWidth == 20)
shelf.updateStateWidth(shelfState, /* fraction */ 1f, /* shortestWidth */ 10)
assertTrue(shelfState.actualWidth == 30)
}
@Test
fun testShelfIsLong_WhenNotOnLockscreen() {
setFractionToShade(0f)
setOnLockscreen(false)
shelf.updateStateWidth(shelfState, /* fraction */ 0f, /* shortestWidth */ 10)
assertTrue(shelfState.actualWidth == 30)
}
@Test
fun testX_inViewForClick() {
val isXInView = shelf.isXInView(
/* localX */ 5f,
/* slop */ 5f,
/* left */ 0f,
/* right */ 10f)
assertTrue(isXInView)
}
@Test
fun testXSlop_inViewForClick() {
val isLeftXSlopInView = shelf.isXInView(
/* localX */ -3f,
/* slop */ 5f,
/* left */ 0f,
/* right */ 10f)
assertTrue(isLeftXSlopInView)
val isRightXSlopInView = shelf.isXInView(
/* localX */ 13f,
/* slop */ 5f,
/* left */ 0f,
/* right */ 10f)
assertTrue(isRightXSlopInView)
}
@Test
fun testX_notInViewForClick() {
val isXLeftOfShelfInView = shelf.isXInView(
/* localX */ -10f,
/* slop */ 5f,
/* left */ 0f,
/* right */ 10f)
assertFalse(isXLeftOfShelfInView)
val isXRightOfShelfInView = shelf.isXInView(
/* localX */ 20f,
/* slop */ 5f,
/* left */ 0f,
/* right */ 10f)
assertFalse(isXRightOfShelfInView)
}
@Test
fun testY_inViewForClick() {
val isYInView = shelf.isYInView(
/* localY */ 5f,
/* slop */ 5f,
/* top */ 0f,
/* bottom */ 10f)
assertTrue(isYInView)
}
@Test
fun testYSlop_inViewForClick() {
val isTopYSlopInView = shelf.isYInView(
/* localY */ -3f,
/* slop */ 5f,
/* top */ 0f,
/* bottom */ 10f)
assertTrue(isTopYSlopInView)
val isBottomYSlopInView = shelf.isYInView(
/* localY */ 13f,
/* slop */ 5f,
/* top */ 0f,
/* bottom */ 10f)
assertTrue(isBottomYSlopInView)
}
@Test
fun testY_notInViewForClick() {
val isYAboveShelfInView = shelf.isYInView(
/* localY */ -10f,
/* slop */ 5f,
/* top */ 0f,
/* bottom */ 5f)
assertFalse(isYAboveShelfInView)
val isYBelowShelfInView = shelf.isYInView(
/* localY */ 15f,
/* slop */ 5f,
/* top */ 0f,
/* bottom */ 5f)
assertFalse(isYBelowShelfInView)
}
private fun setFractionToShade(fraction: Float) {
shelf.setFractionToShade(fraction)
}
private fun setOnLockscreen(isOnLockscreen: Boolean) {
whenever(ambientState.isOnKeyguard).thenReturn(isOnLockscreen)
}
}