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