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