| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.systemui.statusbar.stack; |
| |
| import android.content.Context; |
| import android.util.DisplayMetrics; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import com.android.systemui.R; |
| import com.android.systemui.statusbar.ExpandableNotificationRow; |
| import com.android.systemui.statusbar.ExpandableView; |
| import com.android.systemui.statusbar.policy.HeadsUpManager; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * The Algorithm of the {@link com.android.systemui.statusbar.stack |
| * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar |
| * .stack.StackScrollState} |
| */ |
| public class StackScrollAlgorithm { |
| |
| private static final String LOG_TAG = "StackScrollAlgorithm"; |
| |
| private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; |
| private static final int MAX_ITEMS_IN_TOP_STACK = 3; |
| |
| public static final float DIMMED_SCALE = 0.95f; |
| |
| private int mPaddingBetweenElements; |
| private int mCollapsedSize; |
| private int mTopStackPeekSize; |
| private int mBottomStackPeekSize; |
| private int mZDistanceBetweenElements; |
| private int mZBasicHeight; |
| private int mRoundedRectCornerRadius; |
| |
| private StackIndentationFunctor mTopStackIndentationFunctor; |
| private StackIndentationFunctor mBottomStackIndentationFunctor; |
| |
| private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); |
| private boolean mIsExpansionChanging; |
| private int mFirstChildMaxHeight; |
| private boolean mIsExpanded; |
| private ExpandableView mFirstChildWhileExpanding; |
| private boolean mExpandedOnStart; |
| private int mTopStackTotalSize; |
| private int mPaddingBetweenElementsDimmed; |
| private int mPaddingBetweenElementsNormal; |
| private int mNotificationsTopPadding; |
| private int mBottomStackSlowDownLength; |
| private int mTopStackSlowDownLength; |
| private int mCollapseSecondCardPadding; |
| private boolean mIsSmallScreen; |
| private int mMaxNotificationHeight; |
| private boolean mScaleDimmed; |
| private HeadsUpManager mHeadsUpManager; |
| |
| public StackScrollAlgorithm(Context context) { |
| initConstants(context); |
| updatePadding(false); |
| } |
| |
| private void updatePadding(boolean dimmed) { |
| mPaddingBetweenElements = dimmed && mScaleDimmed |
| ? mPaddingBetweenElementsDimmed |
| : mPaddingBetweenElementsNormal; |
| mTopStackTotalSize = mTopStackSlowDownLength + mPaddingBetweenElements |
| + mTopStackPeekSize; |
| mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( |
| MAX_ITEMS_IN_TOP_STACK, |
| mTopStackPeekSize, |
| mTopStackTotalSize - mTopStackPeekSize, |
| 0.5f); |
| mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( |
| MAX_ITEMS_IN_BOTTOM_STACK, |
| mBottomStackPeekSize, |
| getBottomStackSlowDownLength(), |
| 0.5f); |
| } |
| |
| public int getBottomStackSlowDownLength() { |
| return mBottomStackSlowDownLength + mPaddingBetweenElements; |
| } |
| |
| private void initConstants(Context context) { |
| mPaddingBetweenElementsDimmed = context.getResources() |
| .getDimensionPixelSize(R.dimen.notification_padding_dimmed); |
| mPaddingBetweenElementsNormal = context.getResources() |
| .getDimensionPixelSize(R.dimen.notification_padding); |
| mNotificationsTopPadding = context.getResources() |
| .getDimensionPixelSize(R.dimen.notifications_top_padding); |
| mCollapsedSize = context.getResources() |
| .getDimensionPixelSize(R.dimen.notification_min_height); |
| mMaxNotificationHeight = context.getResources() |
| .getDimensionPixelSize(R.dimen.notification_max_height); |
| mTopStackPeekSize = context.getResources() |
| .getDimensionPixelSize(R.dimen.top_stack_peek_amount); |
| mBottomStackPeekSize = context.getResources() |
| .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); |
| mZDistanceBetweenElements = context.getResources() |
| .getDimensionPixelSize(R.dimen.z_distance_between_notifications); |
| mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; |
| mBottomStackSlowDownLength = context.getResources() |
| .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length); |
| mTopStackSlowDownLength = context.getResources() |
| .getDimensionPixelSize(R.dimen.top_stack_slow_down_length); |
| mRoundedRectCornerRadius = context.getResources().getDimensionPixelSize( |
| R.dimen.notification_material_rounded_rect_radius); |
| mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize( |
| R.dimen.notification_collapse_second_card_padding); |
| mScaleDimmed = context.getResources().getDisplayMetrics().densityDpi |
| >= DisplayMetrics.DENSITY_420; |
| } |
| |
| public boolean shouldScaleDimmed() { |
| return mScaleDimmed; |
| } |
| |
| 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. |
| 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); |
| } |
| |
| private void getNotificationChildrenStates(StackScrollState resultState, |
| StackScrollAlgorithmState algorithmState) { |
| int childCount = algorithmState.visibleChildren.size(); |
| for (int i = 0; i < childCount; i++) { |
| ExpandableView v = algorithmState.visibleChildren.get(i); |
| if (v instanceof ExpandableNotificationRow) { |
| ExpandableNotificationRow row = (ExpandableNotificationRow) v; |
| row.getChildrenStates(resultState); |
| } |
| } |
| } |
| |
| private void updateSpeedBumpState(StackScrollState resultState, |
| StackScrollAlgorithmState algorithmState, int speedBumpIndex) { |
| int childCount = algorithmState.visibleChildren.size(); |
| for (int i = 0; i < childCount; i++) { |
| View child = algorithmState.visibleChildren.get(i); |
| StackViewState childViewState = resultState.getViewStateForView(child); |
| |
| // The speed bump can also be gone, so equality needs to be taken when comparing |
| // indices. |
| childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex; |
| } |
| } |
| |
| private void updateClipping(StackScrollState resultState, |
| StackScrollAlgorithmState algorithmState, AmbientState ambientState) { |
| 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; |
| } |
| } |
| } |
| } |
| |
| public static boolean canChildBeDismissed(View v) { |
| final View veto = v.findViewById(R.id.veto); |
| return (veto != null && veto.getVisibility() != View.GONE); |
| } |
| |
| /** |
| * Updates the shadow outline and the clipping for a view. |
| * |
| * @param state the viewState 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 backgroundHeight the desired background height. The shadows of the view will be |
| * based on this height and the content will be clipped from the top |
| */ |
| private void updateChildClippingAndBackground(StackViewState state, float realHeight, |
| float clipHeight, float backgroundHeight) { |
| if (realHeight > clipHeight) { |
| // Rather overlap than create a hole. |
| state.topOverLap = (int) Math.floor((realHeight - clipHeight) / state.scale); |
| } else { |
| state.topOverLap = 0; |
| } |
| if (realHeight > backgroundHeight) { |
| // Rather overlap than create a hole. |
| state.clipTopAmount = (int) Math.floor((realHeight - backgroundHeight) / state.scale); |
| } else { |
| state.clipTopAmount = 0; |
| } |
| } |
| |
| /** |
| * Updates the dimmed, activated and hiding sensitive states of the children. |
| */ |
| private void updateDimmedActivatedHideSensitive(AmbientState ambientState, |
| StackScrollState resultState, StackScrollAlgorithmState algorithmState) { |
| 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; |
| } |
| } |
| } |
| |
| /** |
| * Handle the special state when views are being dragged |
| */ |
| private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState, |
| StackScrollAlgorithmState algorithmState) { |
| 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(); |
| } |
| } |
| } |
| |
| /** |
| * Update the visible children on the state. |
| */ |
| private void updateVisibleChildren(StackScrollState resultState, |
| StackScrollAlgorithmState state) { |
| ViewGroup hostView = resultState.getHostView(); |
| int childCount = hostView.getChildCount(); |
| state.visibleChildren.clear(); |
| state.visibleChildren.ensureCapacity(childCount); |
| int notGoneIndex = 0; |
| for (int i = 0; i < childCount; i++) { |
| ExpandableView v = (ExpandableView) hostView.getChildAt(i); |
| if (v.getVisibility() != View.GONE) { |
| notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); |
| if (v instanceof ExpandableNotificationRow) { |
| ExpandableNotificationRow row = (ExpandableNotificationRow) v; |
| |
| // handle the notgoneIndex for the children as well |
| List<ExpandableNotificationRow> children = |
| row.getNotificationChildren(); |
| if (row.areChildrenExpanded() && children != null) { |
| for (ExpandableNotificationRow childRow : children) { |
| if (childRow.getVisibility() != View.GONE) { |
| StackViewState childState |
| = resultState.getViewStateForView(childRow); |
| childState.notGoneIndex = notGoneIndex; |
| notGoneIndex++; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| private int updateNotGoneIndex(StackScrollState resultState, |
| StackScrollAlgorithmState state, int notGoneIndex, |
| ExpandableView v) { |
| StackViewState viewState = resultState.getViewStateForView(v); |
| viewState.notGoneIndex = notGoneIndex; |
| state.visibleChildren.add(v); |
| notGoneIndex++; |
| return notGoneIndex; |
| } |
| |
| /** |
| * 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) { |
| |
| // 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); |
| } |
| |
| private void updateHeadsUpStates(StackScrollState resultState, |
| StackScrollAlgorithmState algorithmState, AmbientState ambientState) { |
| 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; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Clamp the yTranslation both up and down to valid positions. |
| * |
| * @param childViewState the view state of the child |
| * @param childHeight the height of this child |
| */ |
| private void clampYTranslation(StackViewState childViewState, int childHeight, |
| AmbientState ambientState) { |
| clampPositionToBottomStackStart(childViewState, childHeight, ambientState); |
| 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) { |
| childViewState.yTranslation = Math.min(childViewState.yTranslation, |
| ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding |
| - 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(StackViewState childViewState, |
| int childHeight) { |
| childViewState.yTranslation = Math.max(childViewState.yTranslation, |
| mCollapsedSize - childHeight); |
| } |
| |
| private int getMaxAllowedChildHeight(View child, AmbientState ambientState) { |
| 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 updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, |
| float transitioningPositionStart, float bottomPeakStart, float currentYPosition, |
| StackViewState childViewState, int childHeight) { |
| |
| // 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; |
| int newHeight = childHeight; |
| if (childHeight > mCollapsedSize && mIsSmallScreen) { |
| newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset - |
| mPaddingBetweenElements - currentYPosition, childHeight), mCollapsedSize); |
| childViewState.height = newHeight; |
| } |
| childViewState.yTranslation = transitioningPositionStart + offset - newHeight |
| - mPaddingBetweenElements; |
| |
| // We want at least to be at the end of the top stack when collapsing |
| clampPositionToTopStackEnd(childViewState, newHeight); |
| childViewState.location = StackViewState.LOCATION_MAIN_AREA; |
| } |
| |
| private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, |
| float transitioningPositionStart, StackViewState childViewState, |
| int childHeight, AmbientState ambientState) { |
| 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); |
| } |
| |
| private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState, |
| int numberOfElementsCompletelyIn, int i, int childHeight, |
| StackViewState childViewState, float scrollOffset) { |
| |
| |
| // First we calculate the index relative to the current stack window of size at most |
| // {@link #MAX_ITEMS_IN_TOP_STACK} |
| int paddedIndex = i - 1 |
| - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0); |
| if (paddedIndex >= 0) { |
| |
| // We are currently visually entering the top stack |
| float distanceToStack = (childHeight + mPaddingBetweenElements) |
| - algorithmState.scrolledPixelsTop; |
| if (i == algorithmState.lastTopStackIndex |
| && distanceToStack > (mTopStackTotalSize + mPaddingBetweenElements)) { |
| |
| // Child is currently translating into stack but not yet inside slow down zone. |
| // Handle it like the regular scrollview. |
| childViewState.yTranslation = scrollOffset; |
| } else { |
| // Apply stacking logic. |
| float numItemsBefore; |
| if (i == algorithmState.lastTopStackIndex) { |
| numItemsBefore = 1.0f |
| - (distanceToStack / (mTopStackTotalSize + mPaddingBetweenElements)); |
| } 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 = StackViewState.LOCATION_TOP_STACK_PEEKING; |
| } else { |
| 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 = StackViewState.LOCATION_TOP_STACK_HIDDEN; |
| } |
| |
| |
| } |
| |
| /** |
| * 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) { |
| |
| // 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; |
| } |
| } |
| |
| /** |
| * Calculate the Z positions for all children based on the number of items in both stacks and |
| * save it in the resultState |
| * |
| * @param resultState The result state to update the zTranslation values |
| * @param algorithmState The state in which the current pass of the algorithm is currently in |
| */ |
| private void updateZValuesForState(StackScrollState resultState, |
| StackScrollAlgorithmState algorithmState) { |
| int childCount = algorithmState.visibleChildren.size(); |
| for (int i = 0; i < childCount; i++) { |
| View child = algorithmState.visibleChildren.get(i); |
| StackViewState childViewState = resultState.getViewStateForView(child); |
| if (i < algorithmState.itemsInTopStack) { |
| float stackIndex = algorithmState.itemsInTopStack - i; |
| |
| // Ensure that the topmost item is a little bit higher than the rest when fully |
| // scrolled, to avoid drawing errors when swiping it out |
| float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2); |
| stackIndex = Math.min(stackIndex, max); |
| if (i == 0 && algorithmState.itemsInTopStack < 2.0f) { |
| |
| // We only have the top item and an additional item in the top stack, |
| // Interpolate the index from 0 to 2 while the second item is |
| // translating in. |
| stackIndex -= 1.0f; |
| if (algorithmState.scrollY > mCollapsedSize) { |
| |
| // Since there is a shadow treshhold, we cant just interpolate from 0 to |
| // 2 but we interpolate from 0.1f to 2.0f when scrolled in. The jump in |
| // height will not be noticable since we have padding in between. |
| stackIndex = 0.1f + stackIndex * 1.9f; |
| } |
| } |
| childViewState.zTranslation = mZBasicHeight |
| + stackIndex * mZDistanceBetweenElements; |
| } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { |
| float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); |
| float translationZ = mZBasicHeight |
| - numItemsAbove * mZDistanceBetweenElements; |
| childViewState.zTranslation = translationZ; |
| } else { |
| childViewState.zTranslation = mZBasicHeight; |
| } |
| } |
| } |
| |
| /** |
| * Update whether the device is very small, i.e. Notifications can be in both the top and the |
| * bottom stack at the same time |
| * |
| * @param panelHeight The normal height of the panel when it's open |
| */ |
| public void updateIsSmallScreen(int panelHeight) { |
| mIsSmallScreen = panelHeight < |
| mCollapsedSize /* top stack */ |
| + mBottomStackSlowDownLength + mBottomStackPeekSize /* bottom stack */ |
| + mMaxNotificationHeight; /* max notification height */ |
| } |
| |
| public void onExpansionStarted(StackScrollState currentState) { |
| mIsExpansionChanging = true; |
| mExpandedOnStart = mIsExpanded; |
| ViewGroup hostView = currentState.getHostView(); |
| updateFirstChildHeightWhileExpanding(hostView); |
| } |
| |
| private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) { |
| mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView); |
| if (mFirstChildWhileExpanding != null) { |
| if (mExpandedOnStart) { |
| |
| // We are collapsing the shade, so the first child can get as most as high as the |
| // current height or the end value of the animation. |
| mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight( |
| mFirstChildWhileExpanding); |
| if (mFirstChildWhileExpanding instanceof ExpandableNotificationRow) { |
| ExpandableNotificationRow row = |
| (ExpandableNotificationRow) mFirstChildWhileExpanding; |
| if (row.isHeadsUp()) { |
| mFirstChildMaxHeight += mCollapsedSize - row.getHeadsUpHeight(); |
| } |
| } |
| } else { |
| updateFirstChildMaxSizeToMaxHeight(); |
| } |
| } else { |
| mFirstChildMaxHeight = 0; |
| } |
| } |
| |
| private void updateFirstChildMaxSizeToMaxHeight() { |
| // We are expanding the shade, expand it to its full height. |
| if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) { |
| |
| // This child was not layouted yet, wait for a layout pass |
| mFirstChildWhileExpanding |
| .addOnLayoutChangeListener(new View.OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, |
| int bottom, int oldLeft, int oldTop, int oldRight, |
| int oldBottom) { |
| if (mFirstChildWhileExpanding != null) { |
| mFirstChildMaxHeight = getMaxAllowedChildHeight( |
| mFirstChildWhileExpanding, null); |
| } else { |
| mFirstChildMaxHeight = 0; |
| } |
| v.removeOnLayoutChangeListener(this); |
| } |
| }); |
| } else { |
| mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding, null); |
| } |
| } |
| |
| private boolean isMaxSizeInitialized(ExpandableView child) { |
| if (child instanceof ExpandableNotificationRow) { |
| ExpandableNotificationRow row = (ExpandableNotificationRow) child; |
| return row.isMaxExpandHeightInitialized(); |
| } |
| return child == null || child.getWidth() != 0; |
| } |
| |
| private View findFirstVisibleChild(ViewGroup container) { |
| int childCount = container.getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| View child = container.getChildAt(i); |
| if (child.getVisibility() != View.GONE) { |
| return child; |
| } |
| } |
| return null; |
| } |
| |
| public void onExpansionStopped() { |
| mIsExpansionChanging = false; |
| mFirstChildWhileExpanding = null; |
| } |
| |
| public void setIsExpanded(boolean isExpanded) { |
| this.mIsExpanded = isExpanded; |
| } |
| |
| public void notifyChildrenChanged(final ViewGroup hostView) { |
| if (mIsExpansionChanging) { |
| hostView.post(new Runnable() { |
| @Override |
| public void run() { |
| updateFirstChildHeightWhileExpanding(hostView); |
| } |
| }); |
| } |
| } |
| |
| public void setDimmed(boolean dimmed) { |
| updatePadding(dimmed); |
| } |
| |
| public void onReset(ExpandableView view) { |
| if (view.equals(mFirstChildWhileExpanding)) { |
| updateFirstChildMaxSizeToMaxHeight(); |
| } |
| } |
| |
| public void setHeadsUpManager(HeadsUpManager headsUpManager) { |
| mHeadsUpManager = headsUpManager; |
| } |
| |
| class StackScrollAlgorithmState { |
| |
| /** |
| * The scroll position of the algorithm |
| */ |
| public int scrollY; |
| |
| /** |
| * The quantity of items which are in the top stack. |
| */ |
| public float itemsInTopStack; |
| |
| /** |
| * how far in is the element currently transitioning into the top stack |
| */ |
| 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. |
| */ |
| public int lastTopStackIndex; |
| |
| /** |
| * The quantity of items which are in the bottom stack. |
| */ |
| public float itemsInBottomStack; |
| |
| /** |
| * how far in is the element currently transitioning into the bottom stack |
| */ |
| public float partialInBottom; |
| |
| /** |
| * The children from the host view which are not gone. |
| */ |
| public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); |
| } |
| |
| } |