StackScrollAlgorithm.java revision 4538cee46d3aa91c85a453049d2b2284b799c66a
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
17package com.android.systemui.statusbar.stack;
18
19import android.content.Context;
20import android.util.DisplayMetrics;
21import android.util.Log;
22import android.view.View;
23import android.view.ViewGroup;
24
25import com.android.systemui.R;
26import com.android.systemui.statusbar.ExpandableNotificationRow;
27import com.android.systemui.statusbar.ExpandableView;
28
29import 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 */
36public 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.
742                mFirstChildMaxHeight = mFirstChildWhileExpanding.getActualHeight();
743            } else {
744                updateFirstChildMaxSizeToMaxHeight();
745            }
746        } else {
747            mFirstChildMaxHeight = 0;
748        }
749    }
750
751    private void updateFirstChildMaxSizeToMaxHeight() {
752        // We are expanding the shade, expand it to its full height.
753        if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) {
754
755            // This child was not layouted yet, wait for a layout pass
756            mFirstChildWhileExpanding
757                    .addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
758                        @Override
759                        public void onLayoutChange(View v, int left, int top, int right,
760                                int bottom, int oldLeft, int oldTop, int oldRight,
761                                int oldBottom) {
762                            if (mFirstChildWhileExpanding != null) {
763                                mFirstChildMaxHeight = getMaxAllowedChildHeight(
764                                        mFirstChildWhileExpanding);
765                            } else {
766                                mFirstChildMaxHeight = 0;
767                            }
768                            v.removeOnLayoutChangeListener(this);
769                        }
770                    });
771        } else {
772            mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding);
773        }
774    }
775
776    private boolean isMaxSizeInitialized(ExpandableView child) {
777        if (child instanceof ExpandableNotificationRow) {
778            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
779            return row.isMaxExpandHeightInitialized();
780        }
781        return child == null || child.getWidth() != 0;
782    }
783
784    private View findFirstVisibleChild(ViewGroup container) {
785        int childCount = container.getChildCount();
786        for (int i = 0; i < childCount; i++) {
787            View child = container.getChildAt(i);
788            if (child.getVisibility() != View.GONE) {
789                return child;
790            }
791        }
792        return null;
793    }
794
795    public void onExpansionStopped() {
796        mIsExpansionChanging = false;
797        mFirstChildWhileExpanding = null;
798    }
799
800    public void setIsExpanded(boolean isExpanded) {
801        this.mIsExpanded = isExpanded;
802    }
803
804    public void notifyChildrenChanged(ViewGroup hostView) {
805        if (mIsExpansionChanging) {
806            updateFirstChildHeightWhileExpanding(hostView);
807        }
808    }
809
810    public void setDimmed(boolean dimmed) {
811        updatePadding(dimmed);
812    }
813
814    public void onReset(ExpandableView view) {
815        if (view.equals(mFirstChildWhileExpanding)) {
816            updateFirstChildMaxSizeToMaxHeight();
817        }
818    }
819
820    class StackScrollAlgorithmState {
821
822        /**
823         * The scroll position of the algorithm
824         */
825        public int scrollY;
826
827        /**
828         *  The quantity of items which are in the top stack.
829         */
830        public float itemsInTopStack;
831
832        /**
833         * how far in is the element currently transitioning into the top stack
834         */
835        public float partialInTop;
836
837        /**
838         * The number of pixels the last child in the top stack has scrolled in to the stack
839         */
840        public float scrolledPixelsTop;
841
842        /**
843         * The last item index which is in the top stack.
844         */
845        public int lastTopStackIndex;
846
847        /**
848         * The quantity of items which are in the bottom stack.
849         */
850        public float itemsInBottomStack;
851
852        /**
853         * how far in is the element currently transitioning into the bottom stack
854         */
855        public float partialInBottom;
856
857        /**
858         * The children from the host view which are not gone.
859         */
860        public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
861    }
862
863}
864