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