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 fb72ac3c1411f..733a9f64fb0ee 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 7776e69d9bed3..4fc49ed26f64c 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 @@ -788,6 +788,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<>(); } /**