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