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