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