Merge "Avoiding intermediate states in NotificationStackScroller"

This commit is contained in:
Selim Cinek
2014-04-22 15:15:04 +00:00
committed by Android (Google) Code Review
6 changed files with 391 additions and 180 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}
// write the algorithm state to the result
resultState.setScrollY(algorithmState.scrollY);
/**
* 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) {
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:
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;
algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
// TODO: only temporarily collapse
if (childHeight != (int) newSize) {
childViewState.height = (int) newSize;
}
childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
float transitioningPositionStart, float bottomPeakStart, float currentYPosition,
StackScrollState.ViewState childViewState, int childHeight) {
return nextYPosition;
// This is the transitioning element on top of bottom stack, calculate how far we are in.
algorithmState.partialInBottom = 1.0f - (
(transitioningPositionStart - currentYPosition) / (childHeight +
mPaddingBetweenElements));
// the offset starting at the transitionPosition of the bottom stack
float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom);
algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
childViewState.yTranslation = transitioningPositionStart + offset - childHeight;
// 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;
currentYPosition = mLayoutHeight;
}
// TODO: only temporarily collapse
if (childHeight != mCollapsedSize) {
childViewState.height = mCollapsedSize;
}
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;
}
@@ -511,11 +583,13 @@ 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;

View File

@@ -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;