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