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