/** Handle the special state when views are being dragged */ private void handleDraggedViews( AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState) { Log.d(TAG, "handleDraggedViews: "); ArrayList<View> draggedViews = ambientState.getDraggedViews(); for (View draggedView : draggedViews) { int childIndex = algorithmState.visibleChildren.indexOf(draggedView); if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) { View nextChild = algorithmState.visibleChildren.get(childIndex + 1); if (!draggedViews.contains(nextChild)) { // only if the view is not dragged itself we modify its state to be fully // visible StackViewState viewState = resultState.getViewStateForView(nextChild); // The child below the dragged one must be fully visible if (ambientState.isShadeExpanded()) { viewState.alpha = 1; } } // Lets set the alpha to the one it currently has, as its currently being dragged StackViewState viewState = resultState.getViewStateForView(draggedView); // The dragged child should keep the set alpha viewState.alpha = draggedView.getAlpha(); } } }
private void updateClipping( StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { Log.d(TAG, "updateClipping: "); boolean dismissAllInProgress = ambientState.isDismissAllInProgress(); float previousNotificationEnd = 0; float previousNotificationStart = 0; boolean previousNotificationIsSwiped = false; int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableView child = algorithmState.visibleChildren.get(i); StackViewState state = resultState.getViewStateForView(child); float newYTranslation = state.yTranslation + state.height * (1f - state.scale) / 2f; float newHeight = state.height * state.scale; // apply clipping and shadow float newNotificationEnd = newYTranslation + newHeight; float clipHeight; if (previousNotificationIsSwiped) { // When the previous notification is swiped, we don't clip the content to the // bottom of it. clipHeight = newHeight; } else { clipHeight = newNotificationEnd - previousNotificationEnd; clipHeight = Math.max(0.0f, clipHeight); if (clipHeight != 0.0f) { // In the unlocked shade we have to clip a little bit higher because of the rounded // corners of the notifications, but only if we are not fully overlapped by // the top card. float clippingCorrection = state.dimmed ? 0 : mRoundedRectCornerRadius * state.scale; clipHeight += clippingCorrection; } } updateChildClippingAndBackground( state, newHeight, clipHeight, newHeight - (previousNotificationStart - newYTranslation)); if (dismissAllInProgress) { state.clipTopAmount = Math.max(child.getMinClipTopAmount(), state.clipTopAmount); } if (!child.isTransparent()) { // Only update the previous values if we are not transparent, // otherwise we would clip to a transparent view. if ((dismissAllInProgress && canChildBeDismissed(child))) { previousNotificationIsSwiped = true; } else { previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child); previousNotificationEnd = newNotificationEnd; previousNotificationStart = newYTranslation + state.clipTopAmount * state.scale; } } } }
private void updateStateForChildFullyInBottomStack( StackScrollAlgorithmState algorithmState, float transitioningPositionStart, StackViewState childViewState, int childHeight, AmbientState ambientState) { Log.d(TAG, "updateStateForChildFullyInBottomStack: "); float currentYPosition; algorithmState.itemsInBottomStack += 1.0f; if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { // We are visually entering the bottom stack currentYPosition = transitioningPositionStart + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) - mPaddingBetweenElements; childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING; } else { // we are fully inside the stack if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { childViewState.alpha = 0.0f; } else if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 1) { childViewState.alpha = 1.0f - algorithmState.partialInBottom; } childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN; currentYPosition = ambientState.getInnerHeight(); } childViewState.yTranslation = currentYPosition - 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( StackViewState childViewState, int childHeight, AmbientState ambientState) { Log.d(TAG, "clampPositionToBottomStackStart: "); childViewState.yTranslation = Math.min( childViewState.yTranslation, ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding - childHeight); }
public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) { // The state of the local variables are saved in an algorithmState to easily subdivide it // into multiple phases. Log.d(TAG, "getStackScrollState: "); StackScrollAlgorithmState algorithmState = mTempAlgorithmState; // First we reset the view states to their default values. resultState.resetViewStates(); algorithmState.itemsInTopStack = 0.0f; algorithmState.partialInTop = 0.0f; algorithmState.lastTopStackIndex = 0; algorithmState.scrolledPixelsTop = 0; algorithmState.itemsInBottomStack = 0.0f; algorithmState.partialInBottom = 0.0f; float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); int scrollY = ambientState.getScrollY(); // Due to the overScroller, the stackscroller can have negative scroll state. This is // already accounted for by the top padding and doesn't need an additional adaption scrollY = Math.max(0, scrollY); algorithmState.scrollY = (int) (scrollY + mCollapsedSize + bottomOverScroll); updateVisibleChildren(resultState, algorithmState); // Phase 1: findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState); // Phase 2: updatePositionsForState(resultState, algorithmState, ambientState); // Phase 3: updateZValuesForState(resultState, algorithmState); handleDraggedViews(ambientState, resultState, algorithmState); updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); updateClipping(resultState, algorithmState, ambientState); updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex()); getNotificationChildrenStates(resultState, algorithmState); }
/** Updates the dimmed, activated and hiding sensitive states of the children. */ private void updateDimmedActivatedHideSensitive( AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState) { Log.d(TAG, "updateDimmedActivatedHideSensitive: "); boolean dimmed = ambientState.isDimmed(); boolean dark = ambientState.isDark(); boolean hideSensitive = ambientState.isHideSensitive(); View activatedChild = ambientState.getActivatedChild(); int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); StackViewState childViewState = resultState.getViewStateForView(child); childViewState.dimmed = dimmed; childViewState.dark = dark; childViewState.hideSensitive = hideSensitive; boolean isActivatedChild = activatedChild == child; childViewState.scale = !mScaleDimmed || !dimmed || isActivatedChild ? 1.0f : DIMMED_SCALE; if (dimmed && isActivatedChild) { childViewState.zTranslation += 2.0f * mZDistanceBetweenElements; } } }
private int getMaxAllowedChildHeight(View child, AmbientState ambientState) { Log.d(TAG, "getMaxAllowedChildHeight: "); if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; if (ambientState == null && row.isHeadsUp() || ambientState != null && ambientState.getTopHeadsUpEntry() == child) { int extraSize = row.getIntrinsicHeight() - row.getHeadsUpHeight(); return mCollapsedSize + extraSize; } return row.getIntrinsicHeight(); } else if (child instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) child; return expandableView.getIntrinsicHeight(); } return child == null ? mCollapsedSize : child.getHeight(); }
private void updateHeadsUpStates( StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { Log.d(TAG, "updateHeadsUpStates: "); int childCount = algorithmState.visibleChildren.size(); ExpandableNotificationRow topHeadsUpEntry = null; for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); if (!(child instanceof ExpandableNotificationRow)) { break; } ExpandableNotificationRow row = (ExpandableNotificationRow) child; if (!row.isHeadsUp()) { break; } else if (topHeadsUpEntry == null) { topHeadsUpEntry = row; } StackViewState childState = resultState.getViewStateForView(row); boolean isTopEntry = topHeadsUpEntry == row; if (mIsExpanded) { if (isTopEntry) { childState.height += row.getHeadsUpHeight() - mCollapsedSize; } childState.height = Math.max(childState.height, row.getHeadsUpHeight()); // Ensure that the heads up is always visible even when scrolled off from the bottom float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height; childState.yTranslation = Math.min(childState.yTranslation, bottomPosition); } if (row.isPinned()) { childState.yTranslation = Math.max(childState.yTranslation, mNotificationsTopPadding); childState.height = row.getHeadsUpHeight(); if (!isTopEntry) { // Ensure that a headsUp doesn't vertically extend further than the heads-up at // the top most z-position StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry); childState.height = row.getHeadsUpHeight(); childState.yTranslation = topState.yTranslation + topState.height - childState.height; } } } }
/** * Find the number of items in the top stack and update the result state if needed. * * @param resultState The result state to update if a height change of an child occurs * @param algorithmState The state in which the current pass of the algorithm is currently in */ private void findNumberOfItemsInTopStackAndUpdateState( StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { Log.d(TAG, "findNumberOfItemsInTopStackAndUpdateState: "); // The y Position if the element would be in a regular scrollView float yPositionInScrollView = 0.0f; int childCount = algorithmState.visibleChildren.size(); // find the number of elements in the top stack. for (int i = 0; i < childCount; i++) { ExpandableView child = algorithmState.visibleChildren.get(i); StackViewState childViewState = resultState.getViewStateForView(child); int childHeight = getMaxAllowedChildHeight(child, ambientState); float yPositionInScrollViewAfterElement = yPositionInScrollView + childHeight + mPaddingBetweenElements; if (yPositionInScrollView < algorithmState.scrollY) { if (i == 0 && algorithmState.scrollY <= mCollapsedSize) { // The starting position of the bottom stack peek int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding; // 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 (i == 0) { childViewState.height = mCollapsedSize; } } else { // According to the regular scroll view we are partially off screen // How much did we scroll into this child algorithmState.scrolledPixelsTop = algorithmState.scrollY - yPositionInScrollView; 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; if (i == 0) { // If it is expanded we have to collapse it to a new size float newSize = yPositionInScrollViewAfterElement - mPaddingBetweenElements - algorithmState.scrollY + mCollapsedSize; newSize = Math.max(mCollapsedSize, newSize); algorithmState.itemsInTopStack = 1.0f; childViewState.height = (int) newSize; } algorithmState.lastTopStackIndex = i; break; } } else { algorithmState.lastTopStackIndex = i - 1; // We are already past the stack so we can end the loop break; } yPositionInScrollView = yPositionInScrollViewAfterElement; } }
/** * Determine the positions for the views. This is the main part of the algorithm. * * @param resultState The result state to update if a change to the properties of a child occurs * @param algorithmState The state in which the current pass of the algorithm is currently in * @param ambientState The current ambient state */ private void updatePositionsForState( StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { Log.d(TAG, "updatePositionsForState: "); // The starting position of the bottom stack peek float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize; // The position where the bottom stack starts. float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength; // The y coordinate of the current child. float currentYPosition = 0.0f; // How far in is the element currently transitioning into the bottom stack. float yPositionInScrollView = 0.0f; // If we have a heads-up higher than the collapsed height we need to add the difference to // the padding of all other elements, i.e push in the top stack slightly. ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry(); int childCount = algorithmState.visibleChildren.size(); int numberOfElementsCompletelyIn = algorithmState.partialInTop == 1.0f ? algorithmState.lastTopStackIndex : (int) algorithmState.itemsInTopStack; for (int i = 0; i < childCount; i++) { ExpandableView child = algorithmState.visibleChildren.get(i); StackViewState childViewState = resultState.getViewStateForView(child); childViewState.location = StackViewState.LOCATION_UNKNOWN; int childHeight = getMaxAllowedChildHeight(child, ambientState); float yPositionInScrollViewAfterElement = yPositionInScrollView + childHeight + mPaddingBetweenElements; 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 updateStateForTopStackChild( algorithmState, numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset); clampPositionToTopStackEnd(childViewState, childHeight); // check if we are overlapping with the bottom stack if (childViewState.yTranslation + childHeight + mPaddingBetweenElements >= bottomStackStart && !mIsExpansionChanging && i != 0 && mIsSmallScreen) { // we just collapse this element slightly int newSize = (int) Math.max( bottomStackStart - mPaddingBetweenElements - childViewState.yTranslation, mCollapsedSize); childViewState.height = newSize; updateStateForChildTransitioningInBottom( algorithmState, bottomStackStart, bottomPeekStart, childViewState.yTranslation, childViewState, childHeight); } clampPositionToBottomStackStart(childViewState, childViewState.height, ambientState); } else if (nextYPosition >= bottomStackStart) { // Case 2: // 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 updateStateForChildFullyInBottomStack( algorithmState, bottomStackStart, childViewState, childHeight, ambientState); } else { // According to the regular scroll view we are currently translating out of / // into the bottom of the screen updateStateForChildTransitioningInBottom( algorithmState, bottomStackStart, bottomPeekStart, currentYPosition, childViewState, childHeight); } } else { // Case 3: // We are in the regular scroll area. childViewState.location = StackViewState.LOCATION_MAIN_AREA; clampYTranslation(childViewState, childHeight, ambientState); } // The first card is always rendered. if (i == 0) { childViewState.alpha = 1.0f; childViewState.yTranslation = Math.max(mCollapsedSize - algorithmState.scrollY, 0); if (childViewState.yTranslation + childViewState.height > bottomPeekStart - mCollapseSecondCardPadding) { childViewState.height = (int) Math.max( bottomPeekStart - mCollapseSecondCardPadding - childViewState.yTranslation, mCollapsedSize); } childViewState.location = StackViewState.LOCATION_FIRST_CARD; } if (childViewState.location == StackViewState.LOCATION_UNKNOWN) { Log.wtf(LOG_TAG, "Failed to assign location for child " + i); } currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements; yPositionInScrollView = yPositionInScrollViewAfterElement; if (ambientState.isShadeExpanded() && topHeadsUpEntry != null && child != topHeadsUpEntry) { childViewState.yTranslation += topHeadsUpEntry.getHeadsUpHeight() - mCollapsedSize; } childViewState.yTranslation += ambientState.getTopPadding() + ambientState.getStackTranslation(); } updateHeadsUpStates(resultState, algorithmState, ambientState); }