From 99c69253389ca38dc36d9a027c8fdad1e4e55ddf Mon Sep 17 00:00:00 2001 From: Lyn Han Date: Wed, 7 Apr 2021 01:09:34 -0500 Subject: [PATCH] Expand notifications like accordian Show all notifications in the same section at the same time. On each invocation of StackScrollAlgorithm - compute fraction of section showing based on current expansion amount - apply that fraction to each view's intrinsic height The notification icon shelf no longer slides down from top of screen, instead it shows if the notification section before it is showing. Bug: 172289889 [Test accordian effect] Test: open shade with single section, no shelf Test: open shade with single section, silent section, no shelf Test: open shade with first section overflowing into shelf Test: open shade with first section, silent section overflowing into shelf Test: open shade with no notifications (empty shade view) Test: open shade with shelf, scroll notifications, then close shade => accordian effect applies to scrolled state [Test for regressions] Test: add/remove delayed notification that arrives while shade opens => animation instantly updates for new notifs (while expansion runs) Test: open shade from heads up notification Test: open shade from pulsing (incoming notif on aod) Test: open shade from lockscreen Change-Id: If3236b9dc202ee75db7cac51a66c49620556ec10 --- .../systemui/statusbar/NotificationShelf.java | 125 +++++-- .../NotificationShelfController.java | 6 +- .../notification/stack/AmbientState.java | 83 ++++- .../stack/NotificationStackScrollLayout.java | 30 +- ...tificationStackScrollLayoutController.java | 4 + .../stack/StackScrollAlgorithm.java | 344 ++++++++++++------ 6 files changed, 446 insertions(+), 146 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index f57fd21526b9c..f4266a27c6b6b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; +import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; import com.android.systemui.statusbar.notification.stack.ViewState; import com.android.systemui.statusbar.phone.NotificationIconContainer; @@ -82,6 +83,9 @@ public class NotificationShelf extends ActivatableNotificationView implements private Rect mClipRect = new Rect(); private int mCutoutHeight; private int mGapHeight; + private int mIndexOfFirstViewInShelf = -1; + private int mIndexOfFirstViewInOverflowingSection = -1; + private NotificationShelfController mController; public NotificationShelf(Context context, AttributeSet attrs) { @@ -159,30 +163,49 @@ public class NotificationShelf extends ActivatableNotificationView implements } /** Update the state of the shelf. */ - public void updateState(AmbientState ambientState) { + public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, + AmbientState ambientState) { ExpandableView lastView = ambientState.getLastVisibleBackgroundChild(); ShelfState viewState = (ShelfState) getViewState(); if (mShowNotificationShelf && lastView != null) { - float maxShelfEnd = ambientState.getInnerHeight() + ambientState.getTopPadding() - + ambientState.getStackTranslation(); ExpandableViewState lastViewState = lastView.getViewState(); - float viewEnd = lastViewState.yTranslation + lastViewState.height; viewState.copyFrom(lastViewState); + viewState.height = getIntrinsicHeight(); - viewState.yTranslation = Math.max(Math.min(viewEnd, maxShelfEnd) - viewState.height, - getFullyClosedTranslation()); viewState.zTranslation = ambientState.getBaseZHeight(); viewState.clipTopAmount = 0; viewState.alpha = 1f - ambientState.getHideAmount(); viewState.belowSpeedBump = mHostLayoutController.getSpeedBumpIndex() == 0; viewState.hideSensitive = false; viewState.xTranslation = getTranslationX(); + viewState.hasItemsInStableShelf = lastViewState.inShelf; + viewState.firstViewInShelf = algorithmState.firstViewInShelf; + viewState.firstViewInOverflowSection = algorithmState.firstViewInOverflowSection; if (mNotGoneIndex != -1) { viewState.notGoneIndex = Math.min(viewState.notGoneIndex, mNotGoneIndex); } - viewState.hasItemsInStableShelf = lastViewState.inShelf; + viewState.hidden = !mAmbientState.isShadeExpanded() - || mAmbientState.isQsCustomizerShowing(); + || mAmbientState.isQsCustomizerShowing() + || algorithmState.firstViewInShelf == null; + + final int indexOfFirstViewInShelf = algorithmState.visibleChildren.indexOf( + algorithmState.firstViewInShelf); + + if (mAmbientState.isExpansionChanging() + && algorithmState.firstViewInShelf != null + && indexOfFirstViewInShelf > 0) { + + // Show shelf if section before it is showing. + final ExpandableView viewBeforeShelf = algorithmState.visibleChildren.get( + indexOfFirstViewInShelf - 1); + if (viewBeforeShelf.getViewState().hidden) { + viewState.hidden = true; + } + } + + final float stackEnd = ambientState.getStackY() + ambientState.getStackHeight(); + viewState.yTranslation = stackEnd - viewState.height; } else { viewState.hidden = true; viewState.location = ExpandableViewState.LOCATION_GONE; @@ -199,13 +222,11 @@ public class NotificationShelf extends ActivatableNotificationView implements if (!mShowNotificationShelf) { return; } - mShelfIcons.resetViewStates(); float shelfStart = getTranslationY(); float numViewsInShelf = 0.0f; View lastChild = mAmbientState.getLastVisibleBackgroundChild(); mNotGoneIndex = -1; - float interpolationStart = mMaxLayoutHeight - getIntrinsicHeight() * 2; // find the first view that doesn't overlap with the shelf int notGoneIndex = 0; int colorOfViewBeforeLast = NO_COLOR; @@ -219,7 +240,7 @@ public class NotificationShelf extends ActivatableNotificationView implements float currentScrollVelocity = mAmbientState.getCurrentScrollVelocity(); boolean scrollingFast = currentScrollVelocity > mScrollFastThreshold || (mAmbientState.isExpansionChanging() - && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold); + && Math.abs(mAmbientState.getExpandingVelocity()) > mScrollFastThreshold); boolean expandingAnimated = mAmbientState.isExpansionChanging() && !mAmbientState.isPanelTracking(); int baseZHeight = mAmbientState.getBaseZHeight(); @@ -233,22 +254,37 @@ public class NotificationShelf extends ActivatableNotificationView implements if (!child.needsClippingToShelf() || child.getVisibility() == GONE) { continue; } - float notificationClipEnd; boolean aboveShelf = ViewState.getFinalTranslationZ(child) > baseZHeight || child.isPinned(); boolean isLastChild = child == lastChild; float rowTranslationY = child.getTranslationY(); + + final float inShelfAmount = updateShelfTransformation(i, child, scrollingFast, + expandingAnimated, isLastChild); + + final float stackEnd = mAmbientState.getStackY() + + mAmbientState.getStackHeight(); + // TODO(b/172289889) scale mPaddingBetweenElements with expansion amount if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) { - notificationClipEnd = shelfStart + getIntrinsicHeight(); + notificationClipEnd = stackEnd; + } else if (mAmbientState.isExpansionChanging()) { + if (mIndexOfFirstViewInOverflowingSection != -1 + && i >= mIndexOfFirstViewInOverflowingSection) { + // Clip notifications in (section overflowing into shelf) to shelf start. + notificationClipEnd = shelfStart - mPaddingBetweenElements; + } else { + // Clip notifications before the section overflowing into shelf + // to stackEnd because we do not show the shelf if the section right before the + // shelf is still hidden. + notificationClipEnd = stackEnd; + } } else { notificationClipEnd = shelfStart - mPaddingBetweenElements; } int clipTop = updateNotificationClipHeight(child, notificationClipEnd, notGoneIndex); clipTopAmount = Math.max(clipTop, clipTopAmount); - final float inShelfAmount = updateShelfTransformation(child, scrollingFast, - expandingAnimated, isLastChild); // If the current row is an ExpandableNotificationRow, update its color, roundedness, // and icon state. if (child instanceof ExpandableNotificationRow) { @@ -314,19 +350,23 @@ public class NotificationShelf extends ActivatableNotificationView implements distanceToGapTop / mGapHeight); previousAnv.setBottomRoundness(firstElementRoundness, false /* don't animate */); - backgroundTop = (int) distanceToGapBottom; } } previousAnv = anv; } } + clipTransientViews(); setClipTopAmount(clipTopAmount); - boolean isHidden = getViewState().hidden || clipTopAmount >= getIntrinsicHeight(); - if (mShowNotificationShelf) { - setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); - } + + boolean isHidden = getViewState().hidden + || clipTopAmount >= getIntrinsicHeight() + || !mShowNotificationShelf + || numViewsInShelf < 1f; + + // TODO(b/172289889) transition last icon in shelf to notification icon and vice versa. + setVisibility(isHidden ? View.INVISIBLE : View.VISIBLE); setBackgroundTop(backgroundTop); setFirstElementRoundness(firstElementRoundness); mShelfIcons.setSpeedBumpIndex(mHostLayoutController.getSpeedBumpIndex()); @@ -339,11 +379,10 @@ public class NotificationShelf extends ActivatableNotificationView implements continue; } ExpandableNotificationRow row = (ExpandableNotificationRow) child; - updateIconClipAmount(row); updateContinuousClipping(row); } - boolean hideBackground = numViewsInShelf < 1.0f; - setHideBackground(hideBackground || backgroundForceHidden); + boolean hideBackground = isHidden; + setHideBackground(hideBackground); if (mNotGoneIndex == -1) { mNotGoneIndex = notGoneIndex; } @@ -476,10 +515,10 @@ public class NotificationShelf extends ActivatableNotificationView implements /** * @return the amount how much this notification is in the shelf */ - private float updateShelfTransformation(ExpandableView view, boolean scrollingFast, + private float updateShelfTransformation(int i, ExpandableView view, boolean scrollingFast, boolean expandingAnimated, boolean isLastChild) { - // Let calculate how much the view is in the shelf + // Let's calculate how much the view is in the shelf float viewStart = view.getTranslationY(); int fullHeight = view.getActualHeight() + mPaddingBetweenElements; float iconTransformStart = calculateIconTransformationStart(view); @@ -496,15 +535,21 @@ public class NotificationShelf extends ActivatableNotificationView implements transformDistance, view.getMinHeight() - getIntrinsicHeight()); } + float viewEnd = viewStart + fullHeight; float fullTransitionAmount = 0.0f; float iconTransitionAmount = 0.0f; float shelfStart = getTranslationY(); - - if (viewEnd >= shelfStart + if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) { + // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf + if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) { + fullTransitionAmount = 1f; + iconTransitionAmount = 1f; + } + } else if (viewEnd >= shelfStart && (!mAmbientState.isUnlockHintRunning() || view.isInShelf()) && (mAmbientState.isShadeExpanded() - || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) { + || (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) { if (viewStart < shelfStart) { float fullAmount = (shelfStart - viewStart) / fullHeight; @@ -572,7 +617,7 @@ public class NotificationShelf extends ActivatableNotificationView implements && !mNoAnimationsInThisFrame; } iconState.clampedAppearAmount = clampedAmount; - setIconTransformationAmount(view, transitionAmount, isLastChild); + setIconTransformationAmount(view, transitionAmount); } private boolean isTargetClipped(ExpandableView view) { @@ -585,12 +630,10 @@ public class NotificationShelf extends ActivatableNotificationView implements + view.getContentTranslation() + view.getRelativeTopPadding(target) + target.getHeight(); - return endOfTarget >= getTranslationY() - mPaddingBetweenElements; } - private void setIconTransformationAmount(ExpandableView view, float transitionAmount, - boolean isLastChild) { + private void setIconTransformationAmount(ExpandableView view, float transitionAmount) { if (!(view instanceof ExpandableNotificationRow)) { return; } @@ -601,7 +644,6 @@ public class NotificationShelf extends ActivatableNotificationView implements return; } iconState.alpha = transitionAmount; - boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); iconState.hidden = isAppearing || (view instanceof ExpandableNotificationRow @@ -610,8 +652,8 @@ public class NotificationShelf extends ActivatableNotificationView implements || (transitionAmount == 0.0f && !iconState.isAnimating(icon)) || row.isAboveShelf() || row.showingPulsing() - || (!row.isInShelf() && isLastChild) || row.getTranslationZ() > mAmbientState.getBaseZHeight(); + iconState.iconAppearAmount = iconState.hidden? 0f : transitionAmount; // Fade in icons at shelf start @@ -790,8 +832,19 @@ public class NotificationShelf extends ActivatableNotificationView implements mController = notificationShelfController; } + public void setIndexOfFirstViewInShelf(ExpandableView firstViewInShelf) { + mIndexOfFirstViewInShelf = mHostLayoutController.indexOfChild(firstViewInShelf); + } + + public void setFirstViewInOverflowingSection(ExpandableView firstViewInOverflowingSection) { + mIndexOfFirstViewInOverflowingSection = + mHostLayoutController.indexOfChild(firstViewInOverflowingSection); + } + private class ShelfState extends ExpandableViewState { private boolean hasItemsInStableShelf; + private ExpandableView firstViewInShelf; + private ExpandableView firstViewInOverflowSection; @Override public void applyToView(View view) { @@ -800,6 +853,8 @@ public class NotificationShelf extends ActivatableNotificationView implements } super.applyToView(view); + setIndexOfFirstViewInShelf(firstViewInShelf); + setFirstViewInOverflowingSection(firstViewInOverflowSection); updateAppearance(); setHasItemsInStableShelf(hasItemsInStableShelf); mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); @@ -812,6 +867,8 @@ public class NotificationShelf extends ActivatableNotificationView implements } super.animateTo(child, properties); + setIndexOfFirstViewInShelf(firstViewInShelf); + setFirstViewInOverflowingSection(firstViewInOverflowSection); updateAppearance(); setHasItemsInStableShelf(hasItemsInStableShelf); mShelfIcons.setAnimationsEnabled(mAnimationsEnabled); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java index 1e935c19b7104..4f70fdb0d9786 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelfController.java @@ -22,6 +22,7 @@ import com.android.systemui.statusbar.notification.row.ActivatableNotificationVi import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; +import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationIconContainer; import com.android.systemui.statusbar.phone.StatusBarNotificationPresenter; @@ -103,9 +104,10 @@ public class NotificationShelfController { return mView.getHeight(); } - public void updateState(AmbientState ambientState) { + public void updateState(StackScrollAlgorithm.StackScrollAlgorithmState algorithmState, + AmbientState ambientState) { mAmbientState = ambientState; - mView.updateState(ambientState); + mView.updateState(algorithmState, ambientState); } public int getIntrinsicHeight() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index 8446b4e6a3f0e..caf47207de073 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -60,7 +60,7 @@ public class AmbientState { private NotificationShelf mShelf; private int mZDistanceBetweenElements; private int mBaseZHeight; - private int mMaxLayoutHeight; + private int mContentHeight; private ExpandableView mLastVisibleBackgroundChild; private float mCurrentScrollVelocity; private int mStatusBarState; @@ -84,6 +84,75 @@ public class AmbientState { private boolean mIsShadeOpening; private float mSectionPadding; + /** Distance of top of notifications panel from top of screen. */ + private float mStackY = 0; + + /** Height of notifications panel. */ + private float mStackHeight = 0; + + /** Fraction of shade expansion. */ + private float mExpansionFraction; + + /** Height of the notifications panel without top padding when expansion completes. */ + private float mStackEndHeight; + + /** + * @return Height of the notifications panel without top padding when expansion completes. + */ + public float getStackEndHeight() { + return mStackEndHeight; + } + + /** + * @param stackEndHeight Height of the notifications panel without top padding + * when expansion completes. + */ + public void setStackEndHeight(float stackEndHeight) { + mStackEndHeight = stackEndHeight; + } + + /** + * @param stackY Distance of top of notifications panel from top of screen. + */ + public void setStackY(float stackY) { + mStackY = stackY; + } + + /** + * @return Distance of top of notifications panel from top of screen. + */ + public float getStackY() { + return mStackY; + } + + /** + * @param expansionFraction Fraction of shade expansion. + */ + public void setExpansionFraction(float expansionFraction) { + mExpansionFraction = expansionFraction; + } + + /** + * @return Fraction of shade expansion. + */ + public float getExpansionFraction() { + return mExpansionFraction; + } + + /** + * @param stackHeight Height of notifications panel. + */ + public void setStackHeight(float stackHeight) { + mStackHeight = stackHeight; + } + + /** + * @return Height of notifications panel. + */ + public float getStackHeight() { + return mStackHeight; + } + /** Tracks the state from AlertingNotificationManager#hasNotifications() */ private boolean mHasAlertEntries; @@ -263,8 +332,8 @@ public class AmbientState { if (mDozeAmount == 1.0f && !isPulseExpanding()) { return mShelf.getHeight(); } - int height = Math.max(mLayoutMinHeight, - Math.min(mLayoutHeight, mMaxLayoutHeight) - mTopPadding); + int height = (int) Math.max(mLayoutMinHeight, + Math.min(mLayoutHeight, mContentHeight) - mTopPadding); if (ignorePulseHeight) { return height; } @@ -313,8 +382,12 @@ public class AmbientState { return mShelf; } - public void setLayoutMaxHeight(int maxLayoutHeight) { - mMaxLayoutHeight = maxLayoutHeight; + public void setContentHeight(int contentHeight) { + mContentHeight = contentHeight; + } + + public float getContentHeight() { + return mContentHeight; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index ad06e7d06270b..6dcdfbe8a7329 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -658,6 +658,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable y = getHeight() - getEmptyBottomMargin(); mDebugPaint.setColor(Color.GREEN); canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + + y = (int) (mAmbientState.getStackY()); + mDebugPaint.setColor(Color.CYAN); + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); + + y = (int) (mAmbientState.getStackY() + mAmbientState.getStackHeight()); + mDebugPaint.setColor(Color.BLUE); + canvas.drawLine(0, y, getWidth(), y, mDebugPaint); } } @@ -1123,11 +1131,25 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mTopPaddingNeedsAnimation = true; mNeedsAnimation = true; } + updateStackPosition(); requestChildrenUpdate(); notifyHeightChangeListener(null, animate); } } + /** + * Apply expansion fraction to the y position and height of the notifications panel. + */ + private void updateStackPosition() { + // Consider interpolating from an mExpansionStartY for use on lockscreen and AOD + mAmbientState.setStackY( + MathUtils.lerp(0, mTopPadding, mAmbientState.getExpansionFraction())); + final float shadeBottom = getHeight() - getEmptyBottomMargin(); + mAmbientState.setStackEndHeight(shadeBottom - mTopPadding); + mAmbientState.setStackHeight( + MathUtils.lerp(0, shadeBottom - mTopPadding, mAmbientState.getExpansionFraction())); + } + /** * Update the height of the panel. * @@ -1135,6 +1157,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ @ShadeViewRefactor(RefactorComponent.COORDINATOR) public void setExpandedHeight(float height) { + final float shadeBottom = getHeight() - getEmptyBottomMargin(); + final float expansionFraction = MathUtils.constrain(height / shadeBottom, 0f, 1f); + mAmbientState.setExpansionFraction(expansionFraction); + updateStackPosition(); + mExpandedHeight = height; setIsExpanded(height > 0); int minExpansionHeight = getMinExpansionHeight(); @@ -2067,7 +2094,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mContentHeight = height + Math.max(mIntrinsicPadding, mTopPadding) + mBottomMargin; updateScrollability(); clampScrollPosition(); - mAmbientState.setLayoutMaxHeight(mContentHeight); + updateStackPosition(); + mAmbientState.setContentHeight(mContentHeight); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index ce7b3979c52d8..4559015059fc2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -783,6 +783,10 @@ public class NotificationStackScrollLayoutController { return mView.getTranslationX(); } + public int indexOfChild(View view) { + return mView.indexOfChild(view); + } + public void setOnHeightChangedListener( ExpandableView.OnHeightChangedListener listener) { mView.setOnHeightChangedListener(listener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index bbdbe809a8d83..3e1a7816c2b2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -20,21 +20,22 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; -import android.util.Log; import android.util.MathUtils; import android.view.View; import android.view.ViewGroup; import com.android.systemui.R; -import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.notification.dagger.SilentHeader; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.FooterView; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * The Algorithm of the {@link com.android.systemui.statusbar.notification.stack @@ -92,20 +93,16 @@ public class StackScrollAlgorithm { // First we reset the view states to their default values. resetChildViewStates(); - initAlgorithmState(mHostView, algorithmState, ambientState); - updatePositionsForState(algorithmState, ambientState); - updateZValuesForState(algorithmState, ambientState); - updateHeadsUpStates(algorithmState, ambientState); updatePulsingStates(algorithmState, ambientState); updateDimmedActivatedHideSensitive(ambientState, algorithmState); updateClipping(algorithmState, ambientState); updateSpeedBumpState(algorithmState, speedBumpIndex); - updateShelfState(ambientState); + updateShelfState(algorithmState, ambientState); getNotificationChildrenStates(algorithmState, ambientState); } @@ -144,10 +141,13 @@ public class StackScrollAlgorithm { } - private void updateShelfState(AmbientState ambientState) { + private void updateShelfState( + StackScrollAlgorithmState algorithmState, + AmbientState ambientState) { + NotificationShelf shelf = ambientState.getShelf(); if (shelf != null) { - shelf.updateState(ambientState); + shelf.updateState(algorithmState, ambientState); } } @@ -172,7 +172,8 @@ public class StackScrollAlgorithm { && ((ExpandableNotificationRow) child).isPinned(); if (mClipNotificationScrollToTop && (!state.inShelf || (isHeadsUp && !firstHeadsUp)) - && newYTranslation < clipStart) { + && newYTranslation < clipStart + && !ambientState.isShadeOpening()) { // The previous view is overlapping on top, clip! float overlapAmount = clipStart - newYTranslation; state.clipTopAmount = (int) overlapAmount; @@ -217,7 +218,6 @@ public class StackScrollAlgorithm { private void initAlgorithmState(ViewGroup hostView, StackScrollAlgorithmState state, AmbientState ambientState) { float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); - int scrollY = ambientState.getScrollY(); // Due to the overScroller, the stackscroller can have negative scroll state. This is @@ -230,7 +230,6 @@ public class StackScrollAlgorithm { state.visibleChildren.clear(); state.visibleChildren.ensureCapacity(childCount); int notGoneIndex = 0; - ExpandableView lastView = null; for (int i = 0; i < childCount; i++) { ExpandableView v = (ExpandableView) hostView.getChildAt(i); if (v.getVisibility() != View.GONE) { @@ -255,12 +254,101 @@ public class StackScrollAlgorithm { } } } - ExpandableNotificationRow expandingNotification = ambientState.getExpandingNotification(); - state.indexOfExpandingNotification = expandingNotification != null - ? expandingNotification.isChildInGroup() - ? state.visibleChildren.indexOf(expandingNotification.getNotificationParent()) - : state.visibleChildren.indexOf(expandingNotification) - : -1; + + state.firstViewInShelf = null; + // Save y, sectionStart, sectionEnd from when shade is fully expanded. + // Consider updating these states in updateContentView instead so that we don't have to + // recalculate in every frame. + float currentY = -scrollY; + int sectionStartIndex = 0; + int sectionEndIndex = 0; + for (int i = 0; i < state.visibleChildren.size(); i++) { + final ExpandableView view = state.visibleChildren.get(i); + // Add space between sections. + final boolean applyGapHeight = childNeedsGapHeight( + ambientState.getSectionProvider(), i, + view, getPreviousView(i, state)); + if (applyGapHeight) { + currentY += mGapHeight; + } + + // Save index of first view in the shelf + final float shelfStart = ambientState.getStackEndHeight() + - ambientState.getShelf().getIntrinsicHeight(); + if (currentY >= shelfStart + && !(view instanceof FooterView) + && state.firstViewInShelf == null) { + state.firstViewInShelf = view; + } + + // Record y position when fully expanded + ExpansionData expansionData = new ExpansionData(); + expansionData.fullyExpandedY = currentY; + state.expansionData.put(view, expansionData); + + if (ambientState.getSectionProvider() + .beginsSection(view, getPreviousView(i, state))) { + + // Save section start/end for views in the section before this new section + ExpandableView sectionStartView = state.visibleChildren.get(sectionStartIndex); + final float sectionStart = + state.expansionData.get(sectionStartView).fullyExpandedY; + + ExpandableView sectionEndView = state.visibleChildren.get(sectionEndIndex); + float sectionEnd = state.expansionData.get(sectionEndView).fullyExpandedY + + sectionEndView.getIntrinsicHeight(); + + // If we show the shelf, trim section end to shelf start + // This means section end > start for views in the shelf + if (state.firstViewInShelf != null && sectionEnd > shelfStart) { + sectionEnd = shelfStart; + } + + // Update section bounds of every view in the previous section + // Consider using shared SectionInfo for views in same section to avoid looping back + for (int j = sectionStartIndex; j < i; j++) { + ExpandableView sectionView = state.visibleChildren.get(j); + ExpansionData viewExpansionData = + state.expansionData.get(sectionView); + viewExpansionData.sectionStart = sectionStart; + viewExpansionData.sectionEnd = sectionEnd; + state.expansionData.put(sectionView, viewExpansionData); + } + sectionStartIndex = i; + + if (view instanceof FooterView) { + // Also record section bounds for FooterView (same as its own) + // because it is the last view and we won't get to this point again + // after the loop ends + ExpansionData footerExpansionData = state.expansionData.get(view); + footerExpansionData.sectionStart = expansionData.fullyExpandedY; + footerExpansionData.sectionEnd = expansionData.fullyExpandedY + + view.getIntrinsicHeight(); + state.expansionData.put(view, footerExpansionData); + } + } + sectionEndIndex = i; + currentY = currentY + + getMaxAllowedChildHeight(view) + + mPaddingBetweenElements; + } + + // Which view starts the section of the view right before the shelf? + // Save it for later when we clip views in that section to shelf start. + state.firstViewInOverflowSection = null; + if (state.firstViewInShelf != null) { + ExpandableView nextView = null; + final int startIndex = state.visibleChildren.indexOf(state.firstViewInShelf); + for (int i = startIndex - 1; i >= 0; i--) { + ExpandableView view = state.visibleChildren.get(i); + if (nextView != null && ambientState.getSectionProvider() + .beginsSection(nextView, view)) { + break; + } + nextView = view; + } + state.firstViewInOverflowSection = nextView; + } } private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, @@ -272,6 +360,10 @@ public class StackScrollAlgorithm { return notGoneIndex; } + private ExpandableView getPreviousView(int i, StackScrollAlgorithmState algorithmState) { + return i > 0 ? algorithmState.visibleChildren.get(i - 1) : null; + } + /** * Determine the positions for the views. This is the main part of the algorithm. * @@ -288,6 +380,15 @@ public class StackScrollAlgorithm { } } + private void setLocation(ExpandableViewState expandableViewState, float currentYPosition, + int i) { + expandableViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; + if (currentYPosition <= 0) { + expandableViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; + } + } + + // TODO(b/172289889) polish shade open from HUN /** * Populates the {@link ExpandableViewState} for a single child. * @@ -306,53 +407,84 @@ public class StackScrollAlgorithm { StackScrollAlgorithmState algorithmState, AmbientState ambientState, float currentYPosition) { - ExpandableView child = algorithmState.visibleChildren.get(i); - ExpandableView previousChild = i > 0 ? algorithmState.visibleChildren.get(i - 1) : null; + + ExpandableView view = algorithmState.visibleChildren.get(i); + ExpandableViewState viewState = view.getViewState(); + viewState.location = ExpandableViewState.LOCATION_UNKNOWN; + viewState.alpha = 1f - ambientState.getHideAmount(); + + if (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(); + } + + // TODO(b/172289889) move sectionFraction and showSection to initAlgorithmState + // Get fraction of section showing, and later apply it to view height and gaps between views + float sectionFraction = 1f; + boolean showSection = true; + + if (!ambientState.isOnKeyguard() + && !ambientState.isPulseExpanding() + && ambientState.isExpansionChanging()) { + + final ExpansionData expansionData = algorithmState.expansionData.get(view); + final float sectionHeight = expansionData.sectionEnd - expansionData.sectionStart; + sectionFraction = MathUtils.constrain( + (ambientState.getStackHeight() - expansionData.sectionStart) / sectionHeight, + 0f, 1f); + showSection = expansionData.sectionStart < ambientState.getStackHeight(); + } + + // Add gap between sections. final boolean applyGapHeight = childNeedsGapHeight( ambientState.getSectionProvider(), i, - child, previousChild); - ExpandableViewState childViewState = child.getViewState(); - childViewState.location = ExpandableViewState.LOCATION_UNKNOWN; - + view, getPreviousView(i, algorithmState)); if (applyGapHeight) { - currentYPosition += mGapHeight; - } - int childHeight = getMaxAllowedChildHeight(child); - childViewState.yTranslation = currentYPosition; - childViewState.alpha = 1f - ambientState.getHideAmount(); - - boolean isFooterView = child instanceof FooterView; - boolean isEmptyShadeView = child instanceof EmptyShadeView; - - childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; - float inset = ambientState.getTopPadding() + ambientState.getStackTranslation() - + ambientState.getSectionPadding(); - if (child.mustStayOnScreen() && childViewState.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 = childViewState.yTranslation + childViewState.height + inset; - childViewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation(); - } - if (isFooterView) { - childViewState.yTranslation = Math.min(childViewState.yTranslation, - ambientState.getInnerHeight() - childHeight); - } else if (isEmptyShadeView) { - childViewState.yTranslation = ambientState.getInnerHeight() - childHeight - + ambientState.getStackTranslation() * 0.25f; - } else if (child != ambientState.getTrackedHeadsUpRow()) { - clampPositionToShelf(child, childViewState, ambientState); + currentYPosition += sectionFraction * mGapHeight; } - currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; - if (currentYPosition <= 0) { - childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; - } - if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) { - Log.wtf(LOG_TAG, "Failed to assign location for child " + i); + viewState.yTranslation = currentYPosition; + + if (view instanceof SectionHeaderView) { + // Add padding before sections for overscroll effect. + viewState.yTranslation += ambientState.getSectionPadding(); } - childViewState.yTranslation += inset; + if (view != ambientState.getTrackedHeadsUpRow()) { + if (ambientState.isExpansionChanging()) { + viewState.hidden = !showSection; + viewState.inShelf = algorithmState.firstViewInShelf != null + && i >= algorithmState.visibleChildren.indexOf( + algorithmState.firstViewInShelf) + && !(view instanceof FooterView); + } else { + // When pulsing (incoming notification on AOD), innerHeight is 0; clamp all + // to shelf start, thereby hiding all notifications (except the first one, which we + // later unhide in updatePulsingState) + final int shelfStart = ambientState.getInnerHeight() + - ambientState.getShelf().getIntrinsicHeight(); + if (!(view instanceof FooterView)) { + viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart); + } + if (viewState.yTranslation >= shelfStart) { + viewState.hidden = !view.isExpandAnimationRunning() + && !view.hasExpandingChild() + && !(view instanceof FooterView); + viewState.inShelf = true; + // Notifications in the shelf cannot be visible HUNs. + viewState.headsUpIsVisible = false; + } + } + viewState.height = (int) MathUtils.lerp( + 0, getMaxAllowedChildHeight(view), sectionFraction); + } + + currentYPosition += viewState.height + sectionFraction * mPaddingBetweenElements; + setLocation(view.getViewState(), currentYPosition, i); + viewState.yTranslation += ambientState.getStackY(); return currentYPosition; } @@ -393,10 +525,10 @@ public class StackScrollAlgorithm { int visibleIndex, View child, View previousChild) { - - boolean needsGapHeight = sectionProvider.beginsSection(child, previousChild) - && visibleIndex > 0; - return needsGapHeight; + return sectionProvider.beginsSection(child, previousChild) + && visibleIndex > 0 + && !(previousChild instanceof SilentHeader) + && !(child instanceof FooterView); } private void updatePulsingStates(StackScrollAlgorithmState algorithmState, @@ -514,42 +646,6 @@ public class StackScrollAlgorithm { childState.yTranslation = newTranslation; } - /** - * Clamp the height of the child down such that its end is at most on the beginning of - * the shelf. - * - * @param childViewState the view state of the child - * @param ambientState the ambient state - */ - private void clampPositionToShelf(ExpandableView child, - ExpandableViewState childViewState, - AmbientState ambientState) { - if (ambientState.getShelf() == null) { - return; - } - - ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow(); - boolean isBeforeTrackedHeadsUp = trackedHeadsUpRow != null - && mHostView.indexOfChild(child) < mHostView.indexOfChild(trackedHeadsUpRow); - - int shelfStart = ambientState.getInnerHeight() - - ambientState.getShelf().getIntrinsicHeight(); - if (ambientState.isAppearing() && !child.isAboveShelf() && !isBeforeTrackedHeadsUp) { - // Don't show none heads-up notifications while in appearing phase. - childViewState.yTranslation = Math.max(childViewState.yTranslation, shelfStart); - } - childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart); - if (child instanceof SectionHeaderView) { - // Add padding before sections for overscroll effect. - childViewState.yTranslation += ambientState.getSectionPadding(); - } - if (childViewState.yTranslation >= shelfStart) { - childViewState.hidden = !child.isExpandAnimationRunning() && !child.hasExpandingChild(); - childViewState.inShelf = true; - childViewState.headsUpIsVisible = false; - } - } - protected int getMaxAllowedChildHeight(View child) { if (child instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) child; @@ -641,6 +737,35 @@ public class StackScrollAlgorithm { this.mIsExpanded = isExpanded; } + /** + * Data used to layout views while shade expansion changes. + */ + public class ExpansionData { + + /** + * Y position of top of first view in section. + */ + public float sectionStart; + + /** + * Y position of bottom of last view in section. + */ + public float sectionEnd; + + /** + * Y position of view when shade is fully expanded. + * Does not include distance between top notifications panel and top of screen. + */ + public float fullyExpandedY; + + /** + * Whether this notification is in the same section as the notification right before the + * shelf. Used to determine which notification should be clipped to shelf start while + * shade expansion changes. + */ + public boolean inOverflowingSection; + } + public class StackScrollAlgorithmState { /** @@ -648,16 +773,27 @@ public class StackScrollAlgorithm { */ public int scrollY; + /** + * First view in shelf. + */ + public ExpandableView firstViewInShelf; + + /** + * First view in section overflowing into shelf while shade expansion changes. + */ + public ExpandableView firstViewInOverflowSection; + + /** + * Map of view to ExpansionData used for layout during shade expansion. + * Use view instead of index as key, because visibleChildren indices do not match the ones + * used in the shelf. + */ + public Map expansionData = new HashMap<>(); + /** * The children from the host view which are not gone. */ - public final ArrayList visibleChildren = new ArrayList(); - - private int indexOfExpandingNotification; - - public int getIndexOfExpandingNotification() { - return indexOfExpandingNotification; - } + public final ArrayList visibleChildren = new ArrayList<>(); } /**