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