diff options
author | Selim Cinek <cinek@google.com> | 2014-04-22 15:19:55 +0000 |
---|---|---|
committer | Android Git Automerger <android-git-automerger@android.com> | 2014-04-22 15:19:55 +0000 |
commit | d304f2b0bb526d88041dd463d2194478362712c0 (patch) | |
tree | 28f08ed864a00a96ff97792ca9a36bedbaf8f04e | |
parent | 4f3693de49c5afdeccd236cba28436260220b6dd (diff) | |
parent | a8fb4a2d00ddadd28c620d424f729eaad02654e0 (diff) | |
download | frameworks_base-d304f2b0bb526d88041dd463d2194478362712c0.zip frameworks_base-d304f2b0bb526d88041dd463d2194478362712c0.tar.gz frameworks_base-d304f2b0bb526d88041dd463d2194478362712c0.tar.bz2 |
am a8fb4a2d: Merge "Avoiding intermediate states in NotificationStackScroller"
* commit 'a8fb4a2d00ddadd28c620d424f729eaad02654e0':
Avoiding intermediate states in NotificationStackScroller
6 files changed, 389 insertions, 178 deletions
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 3047f15..0604817 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -250,7 +250,7 @@ <dimen name="notification_stack_margin_bottom">0dp</dimen> <!-- Space reserved for the cards behind the top card in the top stack --> - <dimen name="top_stack_peek_amount">24dp</dimen> + <dimen name="top_stack_peek_amount">12dp</dimen> <!-- Space reserved for the cards behind the top card in the bottom stack --> <dimen name="bottom_stack_peek_amount">18dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java index 1832d37..8dd3f8d 100644 --- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java @@ -195,6 +195,12 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { mGravity = Gravity.TOP; mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f); mScaleAnimation.setDuration(EXPAND_DURATION); + mScaleAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mCallback.setUserLockedChild(mCurrView, false); + } + }); mPopLimit = mContext.getResources().getDimension(R.dimen.blinds_pop_threshold); mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms); mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min); @@ -549,8 +555,9 @@ public class ExpandHelper implements Gefingerpoken, OnClickListener { mScaleAnimation.setFloatValues(targetHeight); mScaleAnimation.setupStartValues(); mScaleAnimation.start(); + } else { + mCallback.setUserLockedChild(mCurrView, false); } - mCallback.setUserLockedChild(mCurrView, false); mExpanding = false; mExpansionStyle = NONE; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java index fdf4dbf..56f83df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java @@ -54,6 +54,7 @@ public class ExpandableNotificationRow extends FrameLayout private boolean mMaxHeightNeedsUpdate; private NotificationActivator mActivator; private LatestItemView.OnActivatedListener mOnActivatedListener; + private boolean mSelfInitiatedLayout; public ExpandableNotificationRow(Context context, AttributeSet attrs) { super(context, attrs); @@ -162,6 +163,11 @@ public class ExpandableNotificationRow extends FrameLayout } private void updateMaxExpandHeight() { + + // We don't want this method to trigger a layout of the whole view hierarchy, + // as the layout parameters in the end are the same which they were in the beginning. + // Otherwise a loop may occur if this method is called on the layout of a parent. + mSelfInitiatedLayout = true; ViewGroup.LayoutParams lp = getLayoutParams(); int oldHeight = lp.height; lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; @@ -171,6 +177,14 @@ public class ExpandableNotificationRow extends FrameLayout lp.height = oldHeight; setLayoutParams(lp); mMaxExpandHeight = getMeasuredHeight(); + mSelfInitiatedLayout = false; + } + + @Override + public void requestLayout() { + if (!mSelfInitiatedLayout) { + super.requestLayout(); + } } /** @@ -257,4 +271,11 @@ public class ExpandableNotificationRow extends FrameLayout public void setBackgroundResourceIds(int bgResId, int dimmedBgResId) { mLatestItemView.setBackgroundResourceIds(bgResId, dimmedBgResId); } + + /** + * @return the potential height this view could expand in addition. + */ + public int getExpandPotential() { + return getMaximumAllowedExpandHeight() - getHeight(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java index 9800bc9..36d94a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java @@ -20,7 +20,6 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; -import android.graphics.Outline; import android.graphics.Paint; import android.util.AttributeSet; @@ -31,6 +30,7 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.widget.OverScroller; import com.android.systemui.ExpandHelper; @@ -55,7 +55,7 @@ public class NotificationStackScrollLayout extends ViewGroup private static final int INVALID_POINTER = -1; private SwipeHelper mSwipeHelper; - private boolean mSwipingInProgress = true; + private boolean mSwipingInProgress; private int mCurrentStackHeight = Integer.MAX_VALUE; private int mOwnScrollY; private int mMaxLayoutHeight; @@ -73,7 +73,6 @@ public class NotificationStackScrollLayout extends ViewGroup private int mSidePaddings; private Paint mDebugPaint; - private int mBackgroundRoundedRectCornerRadius; private int mContentHeight; private int mCollapsedSize; private int mBottomStackPeekSize; @@ -145,9 +144,6 @@ public class NotificationStackScrollLayout extends ViewGroup mSidePaddings = context.getResources() .getDimensionPixelSize(R.dimen.notification_side_padding); - mBackgroundRoundedRectCornerRadius = context.getResources() - .getDimensionPixelSize( - com.android.internal.R.dimen.notification_quantum_rounded_rect_radius); mCollapsedSize = context.getResources() .getDimensionPixelSize(R.dimen.notification_row_min_height); mBottomStackPeekSize = context.getResources() @@ -177,18 +173,23 @@ public class NotificationStackScrollLayout extends ViewGroup View child = getChildAt(i); float width = child.getMeasuredWidth(); float height = child.getMeasuredHeight(); - int oldWidth = child.getWidth(); - int oldHeight = child.getHeight(); child.layout((int) (centerX - width / 2.0f), 0, (int) (centerX + width / 2.0f), (int) height); - updateChildOutline(child, width, height, oldWidth, oldHeight); } setMaxLayoutHeight(getHeight() - mEmptyMarginBottom); - updateScrollPositionIfNecessary(); - updateChildren(); updateContentHeight(); + getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + updateScrollPositionIfNecessary(); + updateChildren(); + getViewTreeObserver().removeOnPreDrawListener(this); + return true; + } + }); + } public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { @@ -227,7 +228,6 @@ public class NotificationStackScrollLayout extends ViewGroup mCurrentStackScrollState.setScrollY(mOwnScrollY); mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState); mCurrentStackScrollState.apply(); - mOwnScrollY = mCurrentStackScrollState.getScrollY(); if (mListener != null) { mListener.onChildLocationsChanged(this); } @@ -240,31 +240,6 @@ public class NotificationStackScrollLayout extends ViewGroup return false; } - private void updateChildOutline(View child, - float width, - float height, - int oldWidth, - int oldHeight) { - // The children currently have paddings inside themselfs because of the expansion - // visualization. In order for the shadows to work correctly we have to set the correct - // outline. - View container = child.findViewById(R.id.container); - if (container != null && (oldWidth != width || oldHeight != height)) { - Outline outline = getOutlineForSize(container.getLeft(), - container.getTop(), - container.getWidth(), - container.getHeight()); - child.setOutline(outline); - } - } - - private Outline getOutlineForSize(int leftInset, int topInset, int width, int height) { - Outline result = new Outline(); - result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height, - mBackgroundRoundedRectCornerRadius); - return result; - } - private void updateScrollPositionIfNecessary() { int scrollRange = getScrollRange(); if (scrollRange < mOwnScrollY) { @@ -284,7 +259,7 @@ public class NotificationStackScrollLayout extends ViewGroup * * @return either the layout height or the externally defined height, whichever is smaller */ - private float getLayoutHeight() { + private int getLayoutHeight() { return Math.min(mMaxLayoutHeight, mCurrentStackHeight); } @@ -640,14 +615,48 @@ public class NotificationStackScrollLayout extends ViewGroup private int getScrollRange() { int scrollRange = 0; - if (getChildCount() > 0) { + View firstChild = getFirstChildNotGone(); + if (firstChild != null) { int contentHeight = getContentHeight(); - scrollRange = Math.max(0, - contentHeight - mMaxLayoutHeight + mBottomStackPeekSize); + int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); + int firstChildExpandPotential = firstChildMaxExpandHeight - firstChild.getHeight(); + + // If we already scrolled in, the first child is layouted smaller than it actually + // could be when expanded. We have to compensate for this loss of the contentHeight + // by adding the expand potential again. + contentHeight += firstChildExpandPotential; + scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize); + if (scrollRange > 0 && getChildCount() > 0) { + // We want to at least be able collapse the first item and not ending in a weird + // end state. + scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); + } } return scrollRange; } + /** + * @return the first child which has visibility unequal to GONE + */ + private View getFirstChildNotGone() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + return child; + } + } + return null; + } + + private int getMaxExpandHeight(View view) { + if (view instanceof ExpandableNotificationRow) { + ExpandableNotificationRow row = (ExpandableNotificationRow) view; + return row.getMaximumAllowedExpandHeight(); + } + return view.getHeight(); + } + private int getContentHeight() { return mContentHeight; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java index 431f6fe..a7363f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java @@ -20,6 +20,7 @@ import android.content.Context; import android.util.Log; import android.view.View; import android.view.ViewGroup; + import com.android.systemui.R; import com.android.systemui.statusbar.ExpandableNotificationRow; @@ -47,13 +48,14 @@ public class StackScrollAlgorithm { private StackIndentationFunctor mTopStackIndentationFunctor; private StackIndentationFunctor mBottomStackIndentationFunctor; - private float mLayoutHeight; + private int mLayoutHeight; private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); private boolean mIsExpansionChanging; private int mFirstChildMaxHeight; private boolean mIsExpanded; private View mFirstChildWhileExpanding; private boolean mExpandedOnStart; + private int mTopStackTotalSize; public StackScrollAlgorithm(Context context) { initConstants(context); @@ -72,16 +74,16 @@ public class StackScrollAlgorithm { mZDistanceBetweenElements = context.getResources() .getDimensionPixelSize(R.dimen.z_distance_between_notifications); mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; - + mTopStackTotalSize = mCollapsedSize + mPaddingBetweenElements; mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( MAX_ITEMS_IN_TOP_STACK, mTopStackPeekSize, - mCollapsedSize + mPaddingBetweenElements, + mTopStackTotalSize, 0.5f); mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( MAX_ITEMS_IN_BOTTOM_STACK, mBottomStackPeekSize, - mBottomStackPeekSize, + mCollapsedSize + mPaddingBetweenElements + mBottomStackPeekSize, 0.5f); } @@ -94,14 +96,17 @@ public class StackScrollAlgorithm { // First we reset the view states to their default values. resultState.resetViewStates(); - // The first element is always in there so it's initialized with 1.0f; - algorithmState.itemsInTopStack = 1.0f; + algorithmState.itemsInTopStack = 0.0f; algorithmState.partialInTop = 0.0f; algorithmState.lastTopStackIndex = 0; - algorithmState.scrollY = resultState.getScrollY(); + algorithmState.scrolledPixelsTop = 0; algorithmState.itemsInBottomStack = 0.0f; + algorithmState.partialInBottom = 0.0f; + updateVisibleChildren(resultState, algorithmState); + algorithmState.scrollY = getAlgorithmScrollPosition(resultState, algorithmState); + // Phase 1: findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState); @@ -110,9 +115,42 @@ public class StackScrollAlgorithm { // Phase 3: updateZValuesForState(resultState, algorithmState); + } + + /** + * Calculates the scroll offset of the algorithm, based on the resultState. + * + * @param resultState the state to base the calculation on + * @param algorithmState The state in which the current pass of the algorithm is currently in + * @return the scroll offset used for the algorithm + */ + private int getAlgorithmScrollPosition(StackScrollState resultState, + StackScrollAlgorithmState algorithmState) { - // write the algorithm state to the result - resultState.setScrollY(algorithmState.scrollY); + int resultScroll = resultState.getScrollY() + mCollapsedSize; + + // If the first child was collapsed in an earlier pass, we have to decrease the scroll + // position to get into the same state again. + if (algorithmState.visibleChildren.size() > 0) { + View firstView = algorithmState.visibleChildren.get(0); + if (firstView instanceof ExpandableNotificationRow) { + ExpandableNotificationRow firstRow = (ExpandableNotificationRow) firstView; + if (firstRow.isUserLocked()) { + // User is currently modifying this height. + return resultScroll; + } + int scrolledInAmount = 0; + // If the child size was not decreased due to scrolling, we don't substract it, + if (!mIsExpansionChanging) { + scrolledInAmount = firstRow.getExpandPotential(); + } else if (mExpandedOnStart && mFirstChildWhileExpanding == firstView) { + scrolledInAmount = firstRow.getMaximumAllowedExpandHeight() - + mFirstChildMaxHeight; + } + resultScroll -= scrolledInAmount; + } + } + return resultScroll; } /** @@ -141,13 +179,12 @@ public class StackScrollAlgorithm { */ private void updatePositionsForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState) { - float stackHeight = getLayoutHeight(); // The starting position of the bottom stack peek - float bottomPeekStart = stackHeight - mBottomStackPeekSize; + float bottomPeekStart = mLayoutHeight - mBottomStackPeekSize; // The position where the bottom stack starts. - float transitioningPositionStart = bottomPeekStart - mCollapsedSize; + float bottomStackStart = bottomPeekStart - mCollapsedSize; // The y coordinate of the current child. float currentYPosition = 0.0f; @@ -160,76 +197,110 @@ public class StackScrollAlgorithm { for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); StackScrollState.ViewState childViewState = resultState.getViewStateForView(child); - childViewState.yTranslation = currentYPosition; childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN; int childHeight = child.getHeight(); - // The y position after this element - float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements; float yPositionInScrollViewAfterElement = yPositionInScrollView + childHeight + mPaddingBetweenElements; - float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY; - if (i < algorithmState.lastTopStackIndex) { + float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize; + + if (i == algorithmState.lastTopStackIndex + 1) { + // Normally the position of this child is the position in the regular scrollview, + // but if the two stacks are very close to each other, + // then have have to push it even more upwards to the position of the bottom + // stack start. + currentYPosition = Math.min(scrollOffset, bottomStackStart); + } + childViewState.yTranslation = currentYPosition; + + // The y position after this element + float nextYPosition = currentYPosition + childHeight + + mPaddingBetweenElements; + + if (i <= algorithmState.lastTopStackIndex) { // Case 1: // We are in the top Stack - nextYPosition = updateStateForTopStackChild(algorithmState, - numberOfElementsCompletelyIn, - i, childViewState); - } else if (i == algorithmState.lastTopStackIndex) { + updateStateForTopStackChild(algorithmState, + numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset); + clampYTranslation(childViewState, childHeight); + // check if we are overlapping with the bottom stack + if (childViewState.yTranslation + childHeight + mPaddingBetweenElements + >= bottomStackStart && !mIsExpansionChanging) { + // TODO: handle overlapping sizes with end stack better + // we just collapse this element + childViewState.height = mCollapsedSize; + } + } else if (nextYPosition >= bottomStackStart) { // Case 2: - // First element of regular scrollview comes next, so the position is just the - // scrolling position - nextYPosition = updateStateForFirstScrollingChild(transitioningPositionStart, - childViewState, scrollOffset); - } else if (nextYPosition >= transitioningPositionStart) { - if (currentYPosition >= transitioningPositionStart) { - // Case 3: + // We are in the bottom stack. + if (currentYPosition >= bottomStackStart) { // According to the regular scroll view we are fully translated out of the // bottom of the screen so we are fully in the bottom stack - nextYPosition = updateStateForChildFullyInBottomStack(algorithmState, - transitioningPositionStart, childViewState, childHeight); + updateStateForChildFullyInBottomStack(algorithmState, + bottomStackStart, childViewState, childHeight); } else { - // Case 4: // According to the regular scroll view we are currently translating out of / // into the bottom of the screen - nextYPosition = updateStateForChildTransitioningInBottom( - algorithmState, stackHeight, transitioningPositionStart, - currentYPosition, childViewState, - childHeight, nextYPosition); + updateStateForChildTransitioningInBottom(algorithmState, + bottomStackStart, bottomPeekStart, currentYPosition, + childViewState, childHeight); } } else { + // Case 3: + // We are in the regular scroll area. childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; + clampYTranslation(childViewState, childHeight); } + // The first card is always rendered. if (i == 0) { childViewState.alpha = 1.0f; + childViewState.yTranslation = 0; childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD; } if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) { Log.wtf(LOG_TAG, "Failed to assign location for child " + i); } - nextYPosition = Math.max(0, nextYPosition); - currentYPosition = nextYPosition; + currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; yPositionInScrollView = yPositionInScrollViewAfterElement; } } /** - * Update the state for the first child which is in the regular scrolling area. + * Clamp the yTranslation both up and down to valid positions. * - * @param transitioningPositionStart the transition starting position of the bottom stack * @param childViewState the view state of the child - * @param scrollOffset the position in the regular scroll view after this child - * @return the next child position + * @param childHeight the height of this child */ - private float updateStateForFirstScrollingChild(float transitioningPositionStart, - StackScrollState.ViewState childViewState, float scrollOffset) { - childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; - if (scrollOffset < transitioningPositionStart) { - return scrollOffset; - } else { - return transitioningPositionStart; - } + private void clampYTranslation(StackScrollState.ViewState childViewState, int childHeight) { + clampPositionToBottomStackStart(childViewState, childHeight); + clampPositionToTopStackEnd(childViewState, childHeight); + } + + /** + * Clamp the yTranslation of the child down such that its end is at most on the beginning of + * the bottom stack. + * + * @param childViewState the view state of the child + * @param childHeight the height of this child + */ + private void clampPositionToBottomStackStart(StackScrollState.ViewState childViewState, + int childHeight) { + childViewState.yTranslation = Math.min(childViewState.yTranslation, + mLayoutHeight - mBottomStackPeekSize - childHeight); + } + + /** + * Clamp the yTranslation of the child up such that its end is at lest on the end of the top + * stack. + * + * @param childViewState the view state of the child + * @param childHeight the height of this child + */ + private void clampPositionToTopStackEnd(StackScrollState.ViewState childViewState, + int childHeight) { + childViewState.yTranslation = Math.max(childViewState.yTranslation, + mCollapsedSize - childHeight); } private int getMaxAllowedChildHeight(View child) { @@ -240,41 +311,35 @@ public class StackScrollAlgorithm { return child.getHeight(); } - private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, - float stackHeight, float transitioningPositionStart, float currentYPosition, - StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) { - float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition; - newSize = Math.min(childHeight, newSize); - // Transitioning element on top of bottom stack: + private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, + float transitioningPositionStart, float bottomPeakStart, float currentYPosition, + StackScrollState.ViewState childViewState, int childHeight) { + + // This is the transitioning element on top of bottom stack, calculate how far we are in. algorithmState.partialInBottom = 1.0f - ( - (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize); - // Our element can be expanded, so we might even have to scroll further than - // mCollapsedSize - algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom); - float offset = mBottomStackIndentationFunctor.getValue( - algorithmState.partialInBottom); - nextYPosition = transitioningPositionStart + offset; + (transitioningPositionStart - currentYPosition) / (childHeight + + mPaddingBetweenElements)); + + // the offset starting at the transitionPosition of the bottom stack + float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); algorithmState.itemsInBottomStack += algorithmState.partialInBottom; - // TODO: only temporarily collapse - if (childHeight != (int) newSize) { - childViewState.height = (int) newSize; - } - childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; + childViewState.yTranslation = transitioningPositionStart + offset - childHeight; - return nextYPosition; + // We want at least to be at the end of the top stack when collapsing + clampPositionToTopStackEnd(childViewState, childHeight); + childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA; } - private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, + private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, StackScrollState.ViewState childViewState, int childHeight) { - float nextYPosition; + float currentYPosition; algorithmState.itemsInBottomStack += 1.0f; if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { // We are visually entering the bottom stack - nextYPosition = transitioningPositionStart - + mBottomStackIndentationFunctor.getValue( - algorithmState.itemsInBottomStack); + currentYPosition = transitioningPositionStart + + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack); childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING; } else { // we are fully inside the stack @@ -285,43 +350,56 @@ public class StackScrollAlgorithm { childViewState.alpha = 1.0f - algorithmState.partialInBottom; } childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN; - nextYPosition = transitioningPositionStart + mBottomStackPeekSize; - } - // TODO: only temporarily collapse - if (childHeight != mCollapsedSize) { - childViewState.height = mCollapsedSize; + currentYPosition = mLayoutHeight; } - return nextYPosition; + childViewState.yTranslation = currentYPosition - childHeight; + clampPositionToTopStackEnd(childViewState, childHeight); } - private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, - int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) { + private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, + int numberOfElementsCompletelyIn, int i, int childHeight, + StackScrollState.ViewState childViewState, float scrollOffset) { - float nextYPosition = 0; // First we calculate the index relative to the current stack window of size at most // {@link #MAX_ITEMS_IN_TOP_STACK} - int paddedIndex = i + int paddedIndex = i - 1 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); if (paddedIndex >= 0) { + // We are currently visually entering the top stack - nextYPosition = mCollapsedSize + mPaddingBetweenElements - - mTopStackIndentationFunctor.getValue( - algorithmState.itemsInTopStack - i - 1); - nextYPosition = Math.min(nextYPosition, mLayoutHeight - mCollapsedSize - - mBottomStackPeekSize); - if (paddedIndex == 0) { - childViewState.alpha = 1.0f - algorithmState.partialInTop; - childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; + float distanceToStack = childHeight - algorithmState.scrolledPixelsTop; + if (i == algorithmState.lastTopStackIndex && distanceToStack > mTopStackTotalSize) { + + // Child is currently translating into stack but not yet inside slow down zone. + // Handle it like the regular scrollview. + childViewState.yTranslation = scrollOffset; } else { - childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; + // Apply stacking logic. + float numItemsBefore; + if (i == algorithmState.lastTopStackIndex) { + numItemsBefore = 1.0f - (distanceToStack / mTopStackTotalSize); + } else { + numItemsBefore = algorithmState.itemsInTopStack - i; + } + // The end position of the current child + float currentChildEndY = mCollapsedSize + mTopStackTotalSize - + mTopStackIndentationFunctor.getValue(numItemsBefore); + childViewState.yTranslation = currentChildEndY - childHeight; } + childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING; } else { - // We are hidden behind the top card and faded out, so we can hide ourselves. - childViewState.alpha = 0.0f; + if (paddedIndex == -1) { + childViewState.alpha = 1.0f - algorithmState.partialInTop; + } else { + // We are hidden behind the top card and faded out, so we can hide ourselves. + childViewState.alpha = 0.0f; + } + childViewState.yTranslation = mCollapsedSize - childHeight; childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN; } - return nextYPosition; + + } /** @@ -347,10 +425,22 @@ public class StackScrollAlgorithm { + childHeight + mPaddingBetweenElements; if (yPositionInScrollView < algorithmState.scrollY) { - if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) { + if (i == 0 && algorithmState.scrollY == mCollapsedSize) { + + // The starting position of the bottom stack peek + int bottomPeekStart = mLayoutHeight - mBottomStackPeekSize; + // Collapse and expand the first child while the shade is being expanded + float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding + ? mFirstChildMaxHeight + : childHeight; + childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), + mCollapsedSize); + algorithmState.itemsInTopStack = 1.0f; + + } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) { // According to the regular scroll view we are fully off screen algorithmState.itemsInTopStack += 1.0f; - if (childHeight != mCollapsedSize) { + if (i == 0) { childViewState.height = mCollapsedSize; } } else { @@ -360,45 +450,27 @@ public class StackScrollAlgorithm { - mPaddingBetweenElements - algorithmState.scrollY; + if (i == 0) { + newSize += mCollapsedSize; + } + // How much did we scroll into this child - algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize + algorithmState.scrolledPixelsTop = childHeight - newSize; + algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight + mPaddingBetweenElements); // Our element can be expanded, so this can get negative algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop); algorithmState.itemsInTopStack += algorithmState.partialInTop; - // TODO: handle overlapping sizes with end stack newSize = Math.max(mCollapsedSize, newSize); - // TODO: only temporarily collapse - if (newSize != childHeight) { + if (i == 0) { childViewState.height = (int) newSize; - - // We decrease scrollY by the same amount we made this child smaller. - // The new scroll position is therefore the start of the element - algorithmState.scrollY = (int) yPositionInScrollView; - resultState.setScrollY(algorithmState.scrollY); - } - if (childHeight > mCollapsedSize) { - // If we are just resizing this child, this element is not treated to be - // transitioning into the stack and therefore it is the last element in - // the stack. - algorithmState.lastTopStackIndex = i; - break; } + algorithmState.lastTopStackIndex = i; + break; } } else { - algorithmState.lastTopStackIndex = i; - if (i == 0) { - - // The starting position of the bottom stack peek - float bottomPeekStart = getLayoutHeight() - mBottomStackPeekSize; - // Collapse and expand the first child while the shade is being expanded - float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding - ? mFirstChildMaxHeight - : childHeight; - childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight), - mCollapsedSize); - } + algorithmState.lastTopStackIndex = i - 1; // We are already past the stack so we can end the loop break; } @@ -435,11 +507,11 @@ public class StackScrollAlgorithm { } } - public float getLayoutHeight() { + public int getLayoutHeight() { return mLayoutHeight; } - public void setLayoutHeight(float layoutHeight) { + public void setLayoutHeight(int layoutHeight) { this.mLayoutHeight = layoutHeight; } @@ -512,10 +584,12 @@ public class StackScrollAlgorithm { public float partialInTop; /** + * The number of pixels the last child in the top stack has scrolled in to the stack + */ + public float scrolledPixelsTop; + + /** * The last item index which is in the top stack. - * NOTE: In the top stack the item after the transitioning element is also in the stack! - * This is needed to ensure a smooth transition between the y position in the regular - * scrollview and the one in the stack. */ public int lastTopStackIndex; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java index 06a08f3..9742e65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/stack/StackScrollState.java @@ -16,10 +16,14 @@ package com.android.systemui.statusbar.stack; +import android.graphics.Outline; +import android.graphics.Rect; import android.util.Log; import android.view.View; import android.view.ViewGroup; +import com.android.systemui.R; + import java.util.HashMap; import java.util.Map; @@ -34,6 +38,10 @@ public class StackScrollState { private final ViewGroup mHostView; private Map<View, ViewState> mStateMap; private int mScrollY; + private final Rect mClipRect = new Rect(); + private int mBackgroundRoundedRectCornerRadius; + private final Outline mChildOutline = new Outline(); + private final int mChildDividerHeight; public int getScrollY() { return mScrollY; @@ -46,6 +54,10 @@ public class StackScrollState { public StackScrollState(ViewGroup hostView) { mHostView = hostView; mStateMap = new HashMap<View, ViewState>(); + mBackgroundRoundedRectCornerRadius = hostView.getResources().getDimensionPixelSize( + com.android.internal.R.dimen.notification_quantum_rounded_rect_radius); + mChildDividerHeight = hostView.getResources().getDimensionPixelSize(R.dimen + .notification_divider_height); } public ViewGroup getHostView() { @@ -83,10 +95,17 @@ public class StackScrollState { */ public void apply() { int numChildren = mHostView.getChildCount(); + float previousNotificationEnd = 0; + float previousNotificationStart = 0; for (int i = 0; i < numChildren; i++) { View child = mHostView.getChildAt(i); ViewState state = mStateMap.get(child); - if (state != null) { + if (state == null) { + Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " + + "to the hostView"); + continue; + } + if (!state.gone) { float alpha = child.getAlpha(); float yTranslation = child.getTranslationY(); float zTranslation = child.getTranslationZ(); @@ -117,7 +136,7 @@ public class StackScrollState { // apply visibility int oldVisibility = child.getVisibility(); int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE; - if (newVisibility != oldVisibility && !state.gone) { + if (newVisibility != oldVisibility) { child.setVisibility(newVisibility); } @@ -135,13 +154,94 @@ public class StackScrollState { if (height != newHeight) { applyNewHeight(child, newHeight); } - } else { - Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " + - "to the hostView"); + + // apply clipping and shadow + float newNotificationEnd = newYTranslation + newHeight; + updateChildClippingAndShadow(child, newHeight, + newNotificationEnd - (previousNotificationEnd - mChildDividerHeight), + newHeight - (previousNotificationStart - newYTranslation)); + + previousNotificationStart = newYTranslation; + previousNotificationEnd = newNotificationEnd; } } } + /** + * Updates the shadow outline and the clipping for a view. + * + * @param child the view to update + * @param realHeight the currently applied height of the view + * @param clipHeight the desired clip height, the rest of the view will be clipped from the top + * @param shadowHeight the desired height of the shadow, the shadow ends on the bottom + */ + private void updateChildClippingAndShadow(View child, int realHeight, float clipHeight, + float shadowHeight) { + if (realHeight > shadowHeight) { + updateChildOutline(child, realHeight, shadowHeight); + } else { + updateChildOutline(child, realHeight, realHeight); + } + if (realHeight > clipHeight) { + updateChildClip(child, realHeight, clipHeight); + } else { + child.setClipBounds(null); + } + } + + /** + * Updates the clipping of a view + * + * @param child the view to update + * @param height the currently applied height of the view + * @param clipHeight the desired clip height, the rest of the view will be clipped from the top + */ + private void updateChildClip(View child, int height, float clipHeight) { + // The children currently have paddings inside themselfs because of the expansion + // visualization. In order for the clipping to work correctly we have to set the correct + // clip rect on the child. + View container = child.findViewById(R.id.container); + if (container != null) { + int clipInset = (int) (height - clipHeight); + mClipRect.set(0, + clipInset, + child.getWidth(), + height); + child.setClipBounds(mClipRect); + } + } + + /** + * Updates the outline of a view + * + * @param child the view to update + * @param height the currently applied height of the view + * @param outlineHeight the desired height of the outline, the outline ends on the bottom + */ + private void updateChildOutline(View child, + int height, + float outlineHeight) { + // The children currently have paddings inside themselfs because of the expansion + // visualization. In order for the shadows to work correctly we have to set the correct + // outline on the child. + View container = child.findViewById(R.id.container); + if (container != null) { + int shadowInset = (int) (height - outlineHeight); + getOutlineForSize(container.getLeft(), + container.getTop() + shadowInset, + container.getWidth(), + container.getHeight() - shadowInset, + mChildOutline); + child.setOutline(mChildOutline); + } + } + + private void getOutlineForSize(int leftInset, int topInset, int width, int height, + Outline result) { + result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height, + mBackgroundRoundedRectCornerRadius); + } + private void applyNewHeight(View child, int newHeight) { ViewGroup.LayoutParams lp = child.getLayoutParams(); lp.height = newHeight; |