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