Home | History | Annotate | Download | only in stack
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.systemui.statusbar.stack;
     18 
     19 import android.content.Context;
     20 import android.util.DisplayMetrics;
     21 import android.util.Log;
     22 import android.view.View;
     23 import android.view.ViewGroup;
     24 
     25 import com.android.systemui.R;
     26 import com.android.systemui.statusbar.ExpandableNotificationRow;
     27 import com.android.systemui.statusbar.ExpandableView;
     28 import com.android.systemui.statusbar.policy.HeadsUpManager;
     29 
     30 import java.util.ArrayList;
     31 import java.util.List;
     32 
     33 /**
     34  * The Algorithm of the {@link com.android.systemui.statusbar.stack
     35  * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
     36  * .stack.StackScrollState}
     37  */
     38 public class StackScrollAlgorithm {
     39 
     40     private static final String LOG_TAG = "StackScrollAlgorithm";
     41 
     42     private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
     43     private static final int MAX_ITEMS_IN_TOP_STACK = 3;
     44 
     45     public static final float DIMMED_SCALE = 0.95f;
     46 
     47     private int mPaddingBetweenElements;
     48     private int mCollapsedSize;
     49     private int mTopStackPeekSize;
     50     private int mBottomStackPeekSize;
     51     private int mZDistanceBetweenElements;
     52     private int mZBasicHeight;
     53     private int mRoundedRectCornerRadius;
     54 
     55     private StackIndentationFunctor mTopStackIndentationFunctor;
     56     private StackIndentationFunctor mBottomStackIndentationFunctor;
     57 
     58     private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
     59     private boolean mIsExpansionChanging;
     60     private int mFirstChildMaxHeight;
     61     private boolean mIsExpanded;
     62     private ExpandableView mFirstChildWhileExpanding;
     63     private boolean mExpandedOnStart;
     64     private int mTopStackTotalSize;
     65     private int mPaddingBetweenElementsDimmed;
     66     private int mPaddingBetweenElementsNormal;
     67     private int mNotificationsTopPadding;
     68     private int mBottomStackSlowDownLength;
     69     private int mTopStackSlowDownLength;
     70     private int mCollapseSecondCardPadding;
     71     private boolean mIsSmallScreen;
     72     private int mMaxNotificationHeight;
     73     private boolean mScaleDimmed;
     74     private HeadsUpManager mHeadsUpManager;
     75 
     76     public StackScrollAlgorithm(Context context) {
     77         initConstants(context);
     78         updatePadding(false);
     79     }
     80 
     81     private void updatePadding(boolean dimmed) {
     82         mPaddingBetweenElements = dimmed && mScaleDimmed
     83                 ? mPaddingBetweenElementsDimmed
     84                 : mPaddingBetweenElementsNormal;
     85         mTopStackTotalSize = mTopStackSlowDownLength + mPaddingBetweenElements
     86                 + mTopStackPeekSize;
     87         mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
     88                 MAX_ITEMS_IN_TOP_STACK,
     89                 mTopStackPeekSize,
     90                 mTopStackTotalSize - mTopStackPeekSize,
     91                 0.5f);
     92         mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
     93                 MAX_ITEMS_IN_BOTTOM_STACK,
     94                 mBottomStackPeekSize,
     95                 getBottomStackSlowDownLength(),
     96                 0.5f);
     97     }
     98 
     99     public int getBottomStackSlowDownLength() {
    100         return mBottomStackSlowDownLength + mPaddingBetweenElements;
    101     }
    102 
    103     private void initConstants(Context context) {
    104         mPaddingBetweenElementsDimmed = context.getResources()
    105                 .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
    106         mPaddingBetweenElementsNormal = context.getResources()
    107                 .getDimensionPixelSize(R.dimen.notification_padding);
    108         mNotificationsTopPadding = context.getResources()
    109                 .getDimensionPixelSize(R.dimen.notifications_top_padding);
    110         mCollapsedSize = context.getResources()
    111                 .getDimensionPixelSize(R.dimen.notification_min_height);
    112         mMaxNotificationHeight = context.getResources()
    113                 .getDimensionPixelSize(R.dimen.notification_max_height);
    114         mTopStackPeekSize = context.getResources()
    115                 .getDimensionPixelSize(R.dimen.top_stack_peek_amount);
    116         mBottomStackPeekSize = context.getResources()
    117                 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
    118         mZDistanceBetweenElements = context.getResources()
    119                 .getDimensionPixelSize(R.dimen.z_distance_between_notifications);
    120         mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
    121         mBottomStackSlowDownLength = context.getResources()
    122                 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length);
    123         mTopStackSlowDownLength = context.getResources()
    124                 .getDimensionPixelSize(R.dimen.top_stack_slow_down_length);
    125         mRoundedRectCornerRadius = context.getResources().getDimensionPixelSize(
    126                 R.dimen.notification_material_rounded_rect_radius);
    127         mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize(
    128                 R.dimen.notification_collapse_second_card_padding);
    129         mScaleDimmed = context.getResources().getDisplayMetrics().densityDpi
    130                 >= DisplayMetrics.DENSITY_XXHIGH;
    131     }
    132 
    133     public boolean shouldScaleDimmed() {
    134         return mScaleDimmed;
    135     }
    136 
    137     public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
    138         // The state of the local variables are saved in an algorithmState to easily subdivide it
    139         // into multiple phases.
    140         StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
    141 
    142         // First we reset the view states to their default values.
    143         resultState.resetViewStates();
    144 
    145         algorithmState.itemsInTopStack = 0.0f;
    146         algorithmState.partialInTop = 0.0f;
    147         algorithmState.lastTopStackIndex = 0;
    148         algorithmState.scrolledPixelsTop = 0;
    149         algorithmState.itemsInBottomStack = 0.0f;
    150         algorithmState.partialInBottom = 0.0f;
    151         float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
    152 
    153         int scrollY = ambientState.getScrollY();
    154 
    155         // Due to the overScroller, the stackscroller can have negative scroll state. This is
    156         // already accounted for by the top padding and doesn't need an additional adaption
    157         scrollY = Math.max(0, scrollY);
    158         algorithmState.scrollY = (int) (scrollY + mCollapsedSize + bottomOverScroll);
    159 
    160         updateVisibleChildren(resultState, algorithmState);
    161 
    162         // Phase 1:
    163         findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState);
    164 
    165         // Phase 2:
    166         updatePositionsForState(resultState, algorithmState, ambientState);
    167 
    168         // Phase 3:
    169         updateZValuesForState(resultState, algorithmState);
    170 
    171         handleDraggedViews(ambientState, resultState, algorithmState);
    172         updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
    173         updateClipping(resultState, algorithmState, ambientState);
    174         updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex());
    175         getNotificationChildrenStates(resultState, algorithmState);
    176     }
    177 
    178     private void getNotificationChildrenStates(StackScrollState resultState,
    179             StackScrollAlgorithmState algorithmState) {
    180         int childCount = algorithmState.visibleChildren.size();
    181         for (int i = 0; i < childCount; i++) {
    182             ExpandableView v = algorithmState.visibleChildren.get(i);
    183             if (v instanceof ExpandableNotificationRow) {
    184                 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
    185                 row.getChildrenStates(resultState);
    186             }
    187         }
    188     }
    189 
    190     private void updateSpeedBumpState(StackScrollState resultState,
    191             StackScrollAlgorithmState algorithmState, int speedBumpIndex) {
    192         int childCount = algorithmState.visibleChildren.size();
    193         for (int i = 0; i < childCount; i++) {
    194             View child = algorithmState.visibleChildren.get(i);
    195             StackViewState childViewState = resultState.getViewStateForView(child);
    196 
    197             // The speed bump can also be gone, so equality needs to be taken when comparing
    198             // indices.
    199             childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex;
    200         }
    201     }
    202 
    203     private void updateClipping(StackScrollState resultState,
    204             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
    205         boolean dismissAllInProgress = ambientState.isDismissAllInProgress();
    206         float previousNotificationEnd = 0;
    207         float previousNotificationStart = 0;
    208         boolean previousNotificationIsSwiped = false;
    209         int childCount = algorithmState.visibleChildren.size();
    210         for (int i = 0; i < childCount; i++) {
    211             ExpandableView child = algorithmState.visibleChildren.get(i);
    212             StackViewState state = resultState.getViewStateForView(child);
    213             float newYTranslation = state.yTranslation + state.height * (1f - state.scale) / 2f;
    214             float newHeight = state.height * state.scale;
    215             // apply clipping and shadow
    216             float newNotificationEnd = newYTranslation + newHeight;
    217 
    218             float clipHeight;
    219             if (previousNotificationIsSwiped) {
    220                 // When the previous notification is swiped, we don't clip the content to the
    221                 // bottom of it.
    222                 clipHeight = newHeight;
    223             } else {
    224                 clipHeight = newNotificationEnd - previousNotificationEnd;
    225                 clipHeight = Math.max(0.0f, clipHeight);
    226                 if (clipHeight != 0.0f) {
    227 
    228                     // In the unlocked shade we have to clip a little bit higher because of the rounded
    229                     // corners of the notifications, but only if we are not fully overlapped by
    230                     // the top card.
    231                     float clippingCorrection = state.dimmed
    232                             ? 0
    233                             : mRoundedRectCornerRadius * state.scale;
    234                     clipHeight += clippingCorrection;
    235                 }
    236             }
    237 
    238             updateChildClippingAndBackground(state, newHeight, clipHeight,
    239                     newHeight - (previousNotificationStart - newYTranslation));
    240 
    241             if (dismissAllInProgress) {
    242                 state.clipTopAmount = Math.max(child.getMinClipTopAmount(), state.clipTopAmount);
    243             }
    244 
    245             if (!child.isTransparent()) {
    246                 // Only update the previous values if we are not transparent,
    247                 // otherwise we would clip to a transparent view.
    248                 if ((dismissAllInProgress && canChildBeDismissed(child))) {
    249                     previousNotificationIsSwiped = true;
    250                 } else {
    251                     previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child);
    252                     previousNotificationEnd = newNotificationEnd;
    253                     previousNotificationStart = newYTranslation + state.clipTopAmount * state.scale;
    254                 }
    255             }
    256         }
    257     }
    258 
    259     public static boolean canChildBeDismissed(View v) {
    260         final View veto = v.findViewById(R.id.veto);
    261         return (veto != null && veto.getVisibility() != View.GONE);
    262     }
    263 
    264     /**
    265      * Updates the shadow outline and the clipping for a view.
    266      *
    267      * @param state the viewState to update
    268      * @param realHeight the currently applied height of the view
    269      * @param clipHeight the desired clip height, the rest of the view will be clipped from the top
    270      * @param backgroundHeight the desired background height. The shadows of the view will be
    271      *                         based on this height and the content will be clipped from the top
    272      */
    273     private void updateChildClippingAndBackground(StackViewState state, float realHeight,
    274             float clipHeight, float backgroundHeight) {
    275         if (realHeight > clipHeight) {
    276             // Rather overlap than create a hole.
    277             state.topOverLap = (int) Math.floor((realHeight - clipHeight) / state.scale);
    278         } else {
    279             state.topOverLap = 0;
    280         }
    281         if (realHeight > backgroundHeight) {
    282             // Rather overlap than create a hole.
    283             state.clipTopAmount = (int) Math.floor((realHeight - backgroundHeight) / state.scale);
    284         } else {
    285             state.clipTopAmount = 0;
    286         }
    287     }
    288 
    289     /**
    290      * Updates the dimmed, activated and hiding sensitive states of the children.
    291      */
    292     private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
    293             StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
    294         boolean dimmed = ambientState.isDimmed();
    295         boolean dark = ambientState.isDark();
    296         boolean hideSensitive = ambientState.isHideSensitive();
    297         View activatedChild = ambientState.getActivatedChild();
    298         int childCount = algorithmState.visibleChildren.size();
    299         for (int i = 0; i < childCount; i++) {
    300             View child = algorithmState.visibleChildren.get(i);
    301             StackViewState childViewState = resultState.getViewStateForView(child);
    302             childViewState.dimmed = dimmed;
    303             childViewState.dark = dark;
    304             childViewState.hideSensitive = hideSensitive;
    305             boolean isActivatedChild = activatedChild == child;
    306             childViewState.scale = !mScaleDimmed || !dimmed || isActivatedChild
    307                     ? 1.0f
    308                     : DIMMED_SCALE;
    309             if (dimmed && isActivatedChild) {
    310                 childViewState.zTranslation += 2.0f * mZDistanceBetweenElements;
    311             }
    312         }
    313     }
    314 
    315     /**
    316      * Handle the special state when views are being dragged
    317      */
    318     private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
    319             StackScrollAlgorithmState algorithmState) {
    320         ArrayList<View> draggedViews = ambientState.getDraggedViews();
    321         for (View draggedView : draggedViews) {
    322             int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
    323             if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
    324                 View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
    325                 if (!draggedViews.contains(nextChild)) {
    326                     // only if the view is not dragged itself we modify its state to be fully
    327                     // visible
    328                     StackViewState viewState = resultState.getViewStateForView(
    329                             nextChild);
    330                     // The child below the dragged one must be fully visible
    331                     if (ambientState.isShadeExpanded()) {
    332                         viewState.alpha = 1;
    333                     }
    334                 }
    335 
    336                 // Lets set the alpha to the one it currently has, as its currently being dragged
    337                 StackViewState viewState = resultState.getViewStateForView(draggedView);
    338                 // The dragged child should keep the set alpha
    339                 viewState.alpha = draggedView.getAlpha();
    340             }
    341         }
    342     }
    343 
    344     /**
    345      * Update the visible children on the state.
    346      */
    347     private void updateVisibleChildren(StackScrollState resultState,
    348             StackScrollAlgorithmState state) {
    349         ViewGroup hostView = resultState.getHostView();
    350         int childCount = hostView.getChildCount();
    351         state.visibleChildren.clear();
    352         state.visibleChildren.ensureCapacity(childCount);
    353         int notGoneIndex = 0;
    354         for (int i = 0; i < childCount; i++) {
    355             ExpandableView v = (ExpandableView) hostView.getChildAt(i);
    356             if (v.getVisibility() != View.GONE) {
    357                 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
    358                 if (v instanceof ExpandableNotificationRow) {
    359                     ExpandableNotificationRow row = (ExpandableNotificationRow) v;
    360 
    361                     // handle the notgoneIndex for the children as well
    362                     List<ExpandableNotificationRow> children =
    363                             row.getNotificationChildren();
    364                     if (row.areChildrenExpanded() && children != null) {
    365                         for (ExpandableNotificationRow childRow : children) {
    366                             if (childRow.getVisibility() != View.GONE) {
    367                                 StackViewState childState
    368                                         = resultState.getViewStateForView(childRow);
    369                                 childState.notGoneIndex = notGoneIndex;
    370                                 notGoneIndex++;
    371                             }
    372                         }
    373                     }
    374                 }
    375             }
    376         }
    377     }
    378 
    379     private int updateNotGoneIndex(StackScrollState resultState,
    380             StackScrollAlgorithmState state, int notGoneIndex,
    381             ExpandableView v) {
    382         StackViewState viewState = resultState.getViewStateForView(v);
    383         viewState.notGoneIndex = notGoneIndex;
    384         state.visibleChildren.add(v);
    385         notGoneIndex++;
    386         return notGoneIndex;
    387     }
    388 
    389     /**
    390      * Determine the positions for the views. This is the main part of the algorithm.
    391      *
    392      * @param resultState The result state to update if a change to the properties of a child occurs
    393      * @param algorithmState The state in which the current pass of the algorithm is currently in
    394      * @param ambientState The current ambient state
    395      */
    396     private void updatePositionsForState(StackScrollState resultState,
    397             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
    398 
    399         // The starting position of the bottom stack peek
    400         float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize;
    401 
    402         // The position where the bottom stack starts.
    403         float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
    404 
    405         // The y coordinate of the current child.
    406         float currentYPosition = 0.0f;
    407 
    408         // How far in is the element currently transitioning into the bottom stack.
    409         float yPositionInScrollView = 0.0f;
    410 
    411         // If we have a heads-up higher than the collapsed height we need to add the difference to
    412         // the padding of all other elements, i.e push in the top stack slightly.
    413         ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry();
    414 
    415         int childCount = algorithmState.visibleChildren.size();
    416         int numberOfElementsCompletelyIn = algorithmState.partialInTop == 1.0f
    417                 ? algorithmState.lastTopStackIndex
    418                 : (int) algorithmState.itemsInTopStack;
    419         for (int i = 0; i < childCount; i++) {
    420             ExpandableView child = algorithmState.visibleChildren.get(i);
    421             StackViewState childViewState = resultState.getViewStateForView(child);
    422             childViewState.location = StackViewState.LOCATION_UNKNOWN;
    423             int childHeight = getMaxAllowedChildHeight(child, ambientState);
    424             float yPositionInScrollViewAfterElement = yPositionInScrollView
    425                     + childHeight
    426                     + mPaddingBetweenElements;
    427             float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize;
    428 
    429             if (i == algorithmState.lastTopStackIndex + 1) {
    430                 // Normally the position of this child is the position in the regular scrollview,
    431                 // but if the two stacks are very close to each other,
    432                 // then have have to push it even more upwards to the position of the bottom
    433                 // stack start.
    434                 currentYPosition = Math.min(scrollOffset, bottomStackStart);
    435             }
    436             childViewState.yTranslation = currentYPosition;
    437 
    438             // The y position after this element
    439             float nextYPosition = currentYPosition + childHeight +
    440                     mPaddingBetweenElements;
    441 
    442             if (i <= algorithmState.lastTopStackIndex) {
    443                 // Case 1:
    444                 // We are in the top Stack
    445                 updateStateForTopStackChild(algorithmState,
    446                         numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset);
    447                 clampPositionToTopStackEnd(childViewState, childHeight);
    448 
    449                 // check if we are overlapping with the bottom stack
    450                 if (childViewState.yTranslation + childHeight + mPaddingBetweenElements
    451                         >= bottomStackStart && !mIsExpansionChanging && i != 0 && mIsSmallScreen) {
    452                     // we just collapse this element slightly
    453                     int newSize = (int) Math.max(bottomStackStart - mPaddingBetweenElements -
    454                             childViewState.yTranslation, mCollapsedSize);
    455                     childViewState.height = newSize;
    456                     updateStateForChildTransitioningInBottom(algorithmState, bottomStackStart,
    457                             bottomPeekStart, childViewState.yTranslation, childViewState,
    458                             childHeight);
    459                 }
    460                 clampPositionToBottomStackStart(childViewState, childViewState.height,
    461                         ambientState);
    462             } else if (nextYPosition >= bottomStackStart) {
    463                 // Case 2:
    464                 // We are in the bottom stack.
    465                 if (currentYPosition >= bottomStackStart) {
    466                     // According to the regular scroll view we are fully translated out of the
    467                     // bottom of the screen so we are fully in the bottom stack
    468                     updateStateForChildFullyInBottomStack(algorithmState,
    469                             bottomStackStart, childViewState, childHeight, ambientState);
    470                 } else {
    471                     // According to the regular scroll view we are currently translating out of /
    472                     // into the bottom of the screen
    473                     updateStateForChildTransitioningInBottom(algorithmState,
    474                             bottomStackStart, bottomPeekStart, currentYPosition,
    475                             childViewState, childHeight);
    476                 }
    477             } else {
    478                 // Case 3:
    479                 // We are in the regular scroll area.
    480                 childViewState.location = StackViewState.LOCATION_MAIN_AREA;
    481                 clampYTranslation(childViewState, childHeight, ambientState);
    482             }
    483 
    484             // The first card is always rendered.
    485             if (i == 0) {
    486                 childViewState.alpha = 1.0f;
    487                 childViewState.yTranslation = Math.max(mCollapsedSize - algorithmState.scrollY, 0);
    488                 if (childViewState.yTranslation + childViewState.height
    489                         > bottomPeekStart - mCollapseSecondCardPadding) {
    490                     childViewState.height = (int) Math.max(
    491                             bottomPeekStart - mCollapseSecondCardPadding
    492                                     - childViewState.yTranslation, mCollapsedSize);
    493                 }
    494                 childViewState.location = StackViewState.LOCATION_FIRST_CARD;
    495             }
    496             if (childViewState.location == StackViewState.LOCATION_UNKNOWN) {
    497                 Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
    498             }
    499             currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
    500             yPositionInScrollView = yPositionInScrollViewAfterElement;
    501 
    502             if (ambientState.isShadeExpanded() && topHeadsUpEntry != null
    503                     && child != topHeadsUpEntry) {
    504                 childViewState.yTranslation += topHeadsUpEntry.getHeadsUpHeight() - mCollapsedSize;
    505             }
    506             childViewState.yTranslation += ambientState.getTopPadding()
    507                     + ambientState.getStackTranslation();
    508         }
    509         updateHeadsUpStates(resultState, algorithmState, ambientState);
    510     }
    511 
    512     private void updateHeadsUpStates(StackScrollState resultState,
    513             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
    514         int childCount = algorithmState.visibleChildren.size();
    515         ExpandableNotificationRow topHeadsUpEntry = null;
    516         for (int i = 0; i < childCount; i++) {
    517             View child = algorithmState.visibleChildren.get(i);
    518             if (!(child instanceof ExpandableNotificationRow)) {
    519                 break;
    520             }
    521             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
    522             if (!row.isHeadsUp()) {
    523                 break;
    524             } else if (topHeadsUpEntry == null) {
    525                 topHeadsUpEntry = row;
    526             }
    527             StackViewState childState = resultState.getViewStateForView(row);
    528             boolean isTopEntry = topHeadsUpEntry == row;
    529             if (mIsExpanded) {
    530                 if (isTopEntry) {
    531                     childState.height += row.getHeadsUpHeight() - mCollapsedSize;
    532                 }
    533                 childState.height = Math.max(childState.height, row.getHeadsUpHeight());
    534                 // Ensure that the heads up is always visible even when scrolled off from the bottom
    535                 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height;
    536                 childState.yTranslation = Math.min(childState.yTranslation,
    537                         bottomPosition);
    538             }
    539             if (row.isPinned()) {
    540                 childState.yTranslation = Math.max(childState.yTranslation,
    541                         mNotificationsTopPadding);
    542                 childState.height = row.getHeadsUpHeight();
    543                 if (!isTopEntry) {
    544                     // Ensure that a headsUp doesn't vertically extend further than the heads-up at
    545                     // the top most z-position
    546                     StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
    547                     childState.height = row.getHeadsUpHeight();
    548                     childState.yTranslation = topState.yTranslation + topState.height
    549                             - childState.height;
    550                 }
    551             }
    552         }
    553     }
    554 
    555     /**
    556      * Clamp the yTranslation both up and down to valid positions.
    557      *
    558      * @param childViewState the view state of the child
    559      * @param childHeight the height of this child
    560      */
    561     private void clampYTranslation(StackViewState childViewState, int childHeight,
    562             AmbientState ambientState) {
    563         clampPositionToBottomStackStart(childViewState, childHeight, ambientState);
    564         clampPositionToTopStackEnd(childViewState, childHeight);
    565     }
    566 
    567     /**
    568      * Clamp the yTranslation of the child down such that its end is at most on the beginning of
    569      * the bottom stack.
    570      *
    571      * @param childViewState the view state of the child
    572      * @param childHeight the height of this child
    573      */
    574     private void clampPositionToBottomStackStart(StackViewState childViewState,
    575             int childHeight, AmbientState ambientState) {
    576         childViewState.yTranslation = Math.min(childViewState.yTranslation,
    577                 ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding
    578                         - childHeight);
    579     }
    580 
    581     /**
    582      * Clamp the yTranslation of the child up such that its end is at lest on the end of the top
    583      * stack.
    584      *
    585      * @param childViewState the view state of the child
    586      * @param childHeight the height of this child
    587      */
    588     private void clampPositionToTopStackEnd(StackViewState childViewState,
    589             int childHeight) {
    590         childViewState.yTranslation = Math.max(childViewState.yTranslation,
    591                 mCollapsedSize - childHeight);
    592     }
    593 
    594     private int getMaxAllowedChildHeight(View child, AmbientState ambientState) {
    595         if (child instanceof ExpandableNotificationRow) {
    596             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
    597             if (ambientState == null && row.isHeadsUp()
    598                     || ambientState != null && ambientState.getTopHeadsUpEntry() == child) {
    599                 int extraSize = row.getIntrinsicHeight() - row.getHeadsUpHeight();
    600                 return mCollapsedSize + extraSize;
    601             }
    602             return row.getIntrinsicHeight();
    603         } else if (child instanceof ExpandableView) {
    604             ExpandableView expandableView = (ExpandableView) child;
    605             return expandableView.getIntrinsicHeight();
    606         }
    607         return child == null? mCollapsedSize : child.getHeight();
    608     }
    609 
    610     private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
    611             float transitioningPositionStart, float bottomPeakStart, float currentYPosition,
    612             StackViewState childViewState, int childHeight) {
    613 
    614         // This is the transitioning element on top of bottom stack, calculate how far we are in.
    615         algorithmState.partialInBottom = 1.0f - (
    616                 (transitioningPositionStart - currentYPosition) / (childHeight +
    617                         mPaddingBetweenElements));
    618 
    619         // the offset starting at the transitionPosition of the bottom stack
    620         float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom);
    621         algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
    622         int newHeight = childHeight;
    623         if (childHeight > mCollapsedSize && mIsSmallScreen) {
    624             newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset -
    625                     mPaddingBetweenElements - currentYPosition, childHeight), mCollapsedSize);
    626             childViewState.height = newHeight;
    627         }
    628         childViewState.yTranslation = transitioningPositionStart + offset - newHeight
    629                 - mPaddingBetweenElements;
    630 
    631         // We want at least to be at the end of the top stack when collapsing
    632         clampPositionToTopStackEnd(childViewState, newHeight);
    633         childViewState.location = StackViewState.LOCATION_MAIN_AREA;
    634     }
    635 
    636     private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
    637             float transitioningPositionStart, StackViewState childViewState,
    638             int childHeight, AmbientState ambientState) {
    639         float currentYPosition;
    640         algorithmState.itemsInBottomStack += 1.0f;
    641         if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
    642             // We are visually entering the bottom stack
    643             currentYPosition = transitioningPositionStart
    644                     + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack)
    645                     - mPaddingBetweenElements;
    646             childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING;
    647         } else {
    648             // we are fully inside the stack
    649             if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
    650                 childViewState.alpha = 0.0f;
    651             } else if (algorithmState.itemsInBottomStack
    652                     > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
    653                 childViewState.alpha = 1.0f - algorithmState.partialInBottom;
    654             }
    655             childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN;
    656             currentYPosition = ambientState.getInnerHeight();
    657         }
    658         childViewState.yTranslation = currentYPosition - childHeight;
    659         clampPositionToTopStackEnd(childViewState, childHeight);
    660     }
    661 
    662     private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
    663             int numberOfElementsCompletelyIn, int i, int childHeight,
    664             StackViewState childViewState, float scrollOffset) {
    665 
    666 
    667         // First we calculate the index relative to the current stack window of size at most
    668         // {@link #MAX_ITEMS_IN_TOP_STACK}
    669         int paddedIndex = i - 1
    670                 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
    671         if (paddedIndex >= 0) {
    672 
    673             // We are currently visually entering the top stack
    674             float distanceToStack = (childHeight + mPaddingBetweenElements)
    675                     - algorithmState.scrolledPixelsTop;
    676             if (i == algorithmState.lastTopStackIndex
    677                     && distanceToStack > (mTopStackTotalSize + mPaddingBetweenElements)) {
    678 
    679                 // Child is currently translating into stack but not yet inside slow down zone.
    680                 // Handle it like the regular scrollview.
    681                 childViewState.yTranslation = scrollOffset;
    682             } else {
    683                 // Apply stacking logic.
    684                 float numItemsBefore;
    685                 if (i == algorithmState.lastTopStackIndex) {
    686                     numItemsBefore = 1.0f
    687                             - (distanceToStack / (mTopStackTotalSize + mPaddingBetweenElements));
    688                 } else {
    689                     numItemsBefore = algorithmState.itemsInTopStack - i;
    690                 }
    691                 // The end position of the current child
    692                 float currentChildEndY = mCollapsedSize + mTopStackTotalSize
    693                         - mTopStackIndentationFunctor.getValue(numItemsBefore);
    694                 childViewState.yTranslation = currentChildEndY - childHeight;
    695             }
    696             childViewState.location = StackViewState.LOCATION_TOP_STACK_PEEKING;
    697         } else {
    698             if (paddedIndex == -1) {
    699                 childViewState.alpha = 1.0f - algorithmState.partialInTop;
    700             } else {
    701                 // We are hidden behind the top card and faded out, so we can hide ourselves.
    702                 childViewState.alpha = 0.0f;
    703             }
    704             childViewState.yTranslation = mCollapsedSize - childHeight;
    705             childViewState.location = StackViewState.LOCATION_TOP_STACK_HIDDEN;
    706         }
    707 
    708 
    709     }
    710 
    711     /**
    712      * Find the number of items in the top stack and update the result state if needed.
    713      *
    714      * @param resultState The result state to update if a height change of an child occurs
    715      * @param algorithmState The state in which the current pass of the algorithm is currently in
    716      */
    717     private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
    718             StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
    719 
    720         // The y Position if the element would be in a regular scrollView
    721         float yPositionInScrollView = 0.0f;
    722         int childCount = algorithmState.visibleChildren.size();
    723 
    724         // find the number of elements in the top stack.
    725         for (int i = 0; i < childCount; i++) {
    726             ExpandableView child = algorithmState.visibleChildren.get(i);
    727             StackViewState childViewState = resultState.getViewStateForView(child);
    728             int childHeight = getMaxAllowedChildHeight(child, ambientState);
    729             float yPositionInScrollViewAfterElement = yPositionInScrollView
    730                     + childHeight
    731                     + mPaddingBetweenElements;
    732             if (yPositionInScrollView < algorithmState.scrollY) {
    733                 if (i == 0 && algorithmState.scrollY <= mCollapsedSize) {
    734 
    735                     // The starting position of the bottom stack peek
    736                     int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize -
    737                             mCollapseSecondCardPadding;
    738                     // Collapse and expand the first child while the shade is being expanded
    739                     float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
    740                             ? mFirstChildMaxHeight
    741                             : childHeight;
    742                     childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight),
    743                             mCollapsedSize);
    744                     algorithmState.itemsInTopStack = 1.0f;
    745 
    746                 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) {
    747                     // According to the regular scroll view we are fully off screen
    748                     algorithmState.itemsInTopStack += 1.0f;
    749                     if (i == 0) {
    750                         childViewState.height = mCollapsedSize;
    751                     }
    752                 } else {
    753                     // According to the regular scroll view we are partially off screen
    754 
    755                     // How much did we scroll into this child
    756                     algorithmState.scrolledPixelsTop = algorithmState.scrollY
    757                             - yPositionInScrollView;
    758                     algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight
    759                             + mPaddingBetweenElements);
    760 
    761                     // Our element can be expanded, so this can get negative
    762                     algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
    763                     algorithmState.itemsInTopStack += algorithmState.partialInTop;
    764 
    765                     if (i == 0) {
    766                         // If it is expanded we have to collapse it to a new size
    767                         float newSize = yPositionInScrollViewAfterElement
    768                                 - mPaddingBetweenElements
    769                                 - algorithmState.scrollY + mCollapsedSize;
    770                         newSize = Math.max(mCollapsedSize, newSize);
    771                         algorithmState.itemsInTopStack = 1.0f;
    772                         childViewState.height = (int) newSize;
    773                     }
    774                     algorithmState.lastTopStackIndex = i;
    775                     break;
    776                 }
    777             } else {
    778                 algorithmState.lastTopStackIndex = i - 1;
    779                 // We are already past the stack so we can end the loop
    780                 break;
    781             }
    782             yPositionInScrollView = yPositionInScrollViewAfterElement;
    783         }
    784     }
    785 
    786     /**
    787      * Calculate the Z positions for all children based on the number of items in both stacks and
    788      * save it in the resultState
    789      *
    790      * @param resultState The result state to update the zTranslation values
    791      * @param algorithmState The state in which the current pass of the algorithm is currently in
    792      */
    793     private void updateZValuesForState(StackScrollState resultState,
    794             StackScrollAlgorithmState algorithmState) {
    795         int childCount = algorithmState.visibleChildren.size();
    796         for (int i = 0; i < childCount; i++) {
    797             View child = algorithmState.visibleChildren.get(i);
    798             StackViewState childViewState = resultState.getViewStateForView(child);
    799             if (i < algorithmState.itemsInTopStack) {
    800                 float stackIndex = algorithmState.itemsInTopStack - i;
    801 
    802                 // Ensure that the topmost item is a little bit higher than the rest when fully
    803                 // scrolled, to avoid drawing errors when swiping it out
    804                 float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2);
    805                 stackIndex = Math.min(stackIndex, max);
    806                 if (i == 0 && algorithmState.itemsInTopStack < 2.0f) {
    807 
    808                     // We only have the top item and an additional item in the top stack,
    809                     // Interpolate the index from 0 to 2 while the second item is
    810                     // translating in.
    811                     stackIndex -= 1.0f;
    812                     if (algorithmState.scrollY > mCollapsedSize) {
    813 
    814                         // Since there is a shadow treshhold, we cant just interpolate from 0 to
    815                         // 2 but we interpolate from 0.1f to 2.0f when scrolled in. The jump in
    816                         // height will not be noticable since we have padding in between.
    817                         stackIndex = 0.1f + stackIndex * 1.9f;
    818                     }
    819                 }
    820                 childViewState.zTranslation = mZBasicHeight
    821                         + stackIndex * mZDistanceBetweenElements;
    822             } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
    823                 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
    824                 float translationZ = mZBasicHeight
    825                         - numItemsAbove * mZDistanceBetweenElements;
    826                 childViewState.zTranslation = translationZ;
    827             } else {
    828                 childViewState.zTranslation = mZBasicHeight;
    829             }
    830         }
    831     }
    832 
    833     /**
    834      * Update whether the device is very small, i.e. Notifications can be in both the top and the
    835      * bottom stack at the same time
    836      *
    837      * @param panelHeight The normal height of the panel when it's open
    838      */
    839     public void updateIsSmallScreen(int panelHeight) {
    840         mIsSmallScreen = panelHeight <
    841                 mCollapsedSize  /* top stack */
    842                 + mBottomStackSlowDownLength + mBottomStackPeekSize /* bottom stack */
    843                 + mMaxNotificationHeight; /* max notification height */
    844     }
    845 
    846     public void onExpansionStarted(StackScrollState currentState) {
    847         mIsExpansionChanging = true;
    848         mExpandedOnStart = mIsExpanded;
    849         ViewGroup hostView = currentState.getHostView();
    850         updateFirstChildHeightWhileExpanding(hostView);
    851     }
    852 
    853     private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
    854         mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView);
    855         if (mFirstChildWhileExpanding != null) {
    856             if (mExpandedOnStart) {
    857 
    858                 // We are collapsing the shade, so the first child can get as most as high as the
    859                 // current height or the end value of the animation.
    860                 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight(
    861                         mFirstChildWhileExpanding);
    862                 if (mFirstChildWhileExpanding instanceof ExpandableNotificationRow) {
    863                     ExpandableNotificationRow row =
    864                             (ExpandableNotificationRow) mFirstChildWhileExpanding;
    865                     if (row.isHeadsUp()) {
    866                         mFirstChildMaxHeight += mCollapsedSize - row.getHeadsUpHeight();
    867                     }
    868                 }
    869             } else {
    870                 updateFirstChildMaxSizeToMaxHeight();
    871             }
    872         } else {
    873             mFirstChildMaxHeight = 0;
    874         }
    875     }
    876 
    877     private void updateFirstChildMaxSizeToMaxHeight() {
    878         // We are expanding the shade, expand it to its full height.
    879         if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) {
    880 
    881             // This child was not layouted yet, wait for a layout pass
    882             mFirstChildWhileExpanding
    883                     .addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    884                         @Override
    885                         public void onLayoutChange(View v, int left, int top, int right,
    886                                 int bottom, int oldLeft, int oldTop, int oldRight,
    887                                 int oldBottom) {
    888                             if (mFirstChildWhileExpanding != null) {
    889                                 mFirstChildMaxHeight = getMaxAllowedChildHeight(
    890                                         mFirstChildWhileExpanding, null);
    891                             } else {
    892                                 mFirstChildMaxHeight = 0;
    893                             }
    894                             v.removeOnLayoutChangeListener(this);
    895                         }
    896                     });
    897         } else {
    898             mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding, null);
    899         }
    900     }
    901 
    902     private boolean isMaxSizeInitialized(ExpandableView child) {
    903         if (child instanceof ExpandableNotificationRow) {
    904             ExpandableNotificationRow row = (ExpandableNotificationRow) child;
    905             return row.isMaxExpandHeightInitialized();
    906         }
    907         return child == null || child.getWidth() != 0;
    908     }
    909 
    910     private View findFirstVisibleChild(ViewGroup container) {
    911         int childCount = container.getChildCount();
    912         for (int i = 0; i < childCount; i++) {
    913             View child = container.getChildAt(i);
    914             if (child.getVisibility() != View.GONE) {
    915                 return child;
    916             }
    917         }
    918         return null;
    919     }
    920 
    921     public void onExpansionStopped() {
    922         mIsExpansionChanging = false;
    923         mFirstChildWhileExpanding = null;
    924     }
    925 
    926     public void setIsExpanded(boolean isExpanded) {
    927         this.mIsExpanded = isExpanded;
    928     }
    929 
    930     public void notifyChildrenChanged(final ViewGroup hostView) {
    931         if (mIsExpansionChanging) {
    932             hostView.post(new Runnable() {
    933                 @Override
    934                 public void run() {
    935                     updateFirstChildHeightWhileExpanding(hostView);
    936                 }
    937             });
    938         }
    939     }
    940 
    941     public void setDimmed(boolean dimmed) {
    942         updatePadding(dimmed);
    943     }
    944 
    945     public void onReset(ExpandableView view) {
    946         if (view.equals(mFirstChildWhileExpanding)) {
    947             updateFirstChildMaxSizeToMaxHeight();
    948         }
    949     }
    950 
    951     public void setHeadsUpManager(HeadsUpManager headsUpManager) {
    952         mHeadsUpManager = headsUpManager;
    953     }
    954 
    955     class StackScrollAlgorithmState {
    956 
    957         /**
    958          * The scroll position of the algorithm
    959          */
    960         public int scrollY;
    961 
    962         /**
    963          *  The quantity of items which are in the top stack.
    964          */
    965         public float itemsInTopStack;
    966 
    967         /**
    968          * how far in is the element currently transitioning into the top stack
    969          */
    970         public float partialInTop;
    971 
    972         /**
    973          * The number of pixels the last child in the top stack has scrolled in to the stack
    974          */
    975         public float scrolledPixelsTop;
    976 
    977         /**
    978          * The last item index which is in the top stack.
    979          */
    980         public int lastTopStackIndex;
    981 
    982         /**
    983          * The quantity of items which are in the bottom stack.
    984          */
    985         public float itemsInBottomStack;
    986 
    987         /**
    988          * how far in is the element currently transitioning into the bottom stack
    989          */
    990         public float partialInBottom;
    991 
    992         /**
    993          * The children from the host view which are not gone.
    994          */
    995         public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
    996     }
    997 
    998 }
    999