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