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