blob: d98bcfe25815a942cf992940f1da1bb261b58e19 [file] [log] [blame]
Selim Cinek67b22602014-03-10 15:40:16 +01001/*
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;
Jorim Jaggid7c1fae2014-08-13 18:27:47 +020020import android.util.DisplayMetrics;
Christoph Studer6e3eceb2014-04-01 18:40:27 +020021import android.util.Log;
Selim Cinek67b22602014-03-10 15:40:16 +010022import android.view.View;
23import android.view.ViewGroup;
Selim Cinek343e6e22014-04-11 21:23:30 +020024
Selim Cinek67b22602014-03-10 15:40:16 +010025import com.android.systemui.R;
Selim Cinek1685e632014-04-08 02:27:49 +020026import com.android.systemui.statusbar.ExpandableNotificationRow;
Jorim Jaggibe565df2014-04-28 17:51:23 +020027import com.android.systemui.statusbar.ExpandableView;
Selim Cinekb8f09cf2015-03-16 17:09:28 -070028import com.android.systemui.statusbar.policy.HeadsUpManager;
Selim Cinek67b22602014-03-10 15:40:16 +010029
Jorim Jaggid4a57442014-04-10 02:45:55 +020030import java.util.ArrayList;
Selim Cinekb5605e52015-02-20 18:21:41 +010031import java.util.List;
Selim Cineka59ecc32015-04-07 10:51:49 -070032import java.util.TreeSet;
Jorim Jaggid4a57442014-04-10 02:45:55 +020033
Selim Cinek67b22602014-03-10 15:40:16 +010034/**
35 * The Algorithm of the {@link com.android.systemui.statusbar.stack
36 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
37 * .stack.StackScrollState}
38 */
39public class StackScrollAlgorithm {
40
Christoph Studer6e3eceb2014-04-01 18:40:27 +020041 private static final String LOG_TAG = "StackScrollAlgorithm";
42
Selim Cinek67b22602014-03-10 15:40:16 +010043 private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
44 private static final int MAX_ITEMS_IN_TOP_STACK = 3;
45
Jorim Jaggi362dd6d2014-07-09 19:04:07 +020046 public static final float DIMMED_SCALE = 0.95f;
Jorim Jaggid552d9d2014-05-07 19:41:13 +020047
Selim Cinek67b22602014-03-10 15:40:16 +010048 private int mPaddingBetweenElements;
49 private int mCollapsedSize;
50 private int mTopStackPeekSize;
51 private int mBottomStackPeekSize;
52 private int mZDistanceBetweenElements;
53 private int mZBasicHeight;
Selim Cinek708a6c12014-05-28 14:16:02 +020054 private int mRoundedRectCornerRadius;
Selim Cinek67b22602014-03-10 15:40:16 +010055
56 private StackIndentationFunctor mTopStackIndentationFunctor;
57 private StackIndentationFunctor mBottomStackIndentationFunctor;
58
Selim Cinek67b22602014-03-10 15:40:16 +010059 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
Selim Cinek1685e632014-04-08 02:27:49 +020060 private boolean mIsExpansionChanging;
61 private int mFirstChildMaxHeight;
62 private boolean mIsExpanded;
Jorim Jaggibe565df2014-04-28 17:51:23 +020063 private ExpandableView mFirstChildWhileExpanding;
Selim Cinek1685e632014-04-08 02:27:49 +020064 private boolean mExpandedOnStart;
Selim Cinek343e6e22014-04-11 21:23:30 +020065 private int mTopStackTotalSize;
Selim Cinek34c0a8d2014-05-12 00:01:43 +020066 private int mPaddingBetweenElementsDimmed;
67 private int mPaddingBetweenElementsNormal;
68 private int mBottomStackSlowDownLength;
Selim Cinekad3e5af2014-07-04 12:24:11 +020069 private int mTopStackSlowDownLength;
Selim Cinekd83771e2014-07-04 16:45:31 +020070 private int mCollapseSecondCardPadding;
Selim Cinek3afd00e2014-08-11 22:32:57 +020071 private boolean mIsSmallScreen;
72 private int mMaxNotificationHeight;
Jorim Jaggid7c1fae2014-08-13 18:27:47 +020073 private boolean mScaleDimmed;
Selim Cinekd2281152015-04-10 14:37:46 -070074 private HeadsUpManager mHeadsUpManager;
Selim Cinek67b22602014-03-10 15:40:16 +010075
76 public StackScrollAlgorithm(Context context) {
77 initConstants(context);
Selim Cinek34c0a8d2014-05-12 00:01:43 +020078 updatePadding(false);
79 }
80
81 private void updatePadding(boolean dimmed) {
Jorim Jaggid7c1fae2014-08-13 18:27:47 +020082 mPaddingBetweenElements = dimmed && mScaleDimmed
Selim Cinek34c0a8d2014-05-12 00:01:43 +020083 ? mPaddingBetweenElementsDimmed
84 : mPaddingBetweenElementsNormal;
Selim Cinekad3e5af2014-07-04 12:24:11 +020085 mTopStackTotalSize = mTopStackSlowDownLength + mPaddingBetweenElements
86 + mTopStackPeekSize;
Selim Cinek34c0a8d2014-05-12 00:01:43 +020087 mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
88 MAX_ITEMS_IN_TOP_STACK,
89 mTopStackPeekSize,
Selim Cinekb96924d2014-05-12 15:11:25 +020090 mTopStackTotalSize - mTopStackPeekSize,
Selim Cinek34c0a8d2014-05-12 00:01:43 +020091 0.5f);
92 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
93 MAX_ITEMS_IN_BOTTOM_STACK,
94 mBottomStackPeekSize,
95 getBottomStackSlowDownLength(),
96 0.5f);
97 }
98
99 public int getBottomStackSlowDownLength() {
100 return mBottomStackSlowDownLength + mPaddingBetweenElements;
Selim Cinek67b22602014-03-10 15:40:16 +0100101 }
102
103 private void initConstants(Context context) {
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200104 mPaddingBetweenElementsDimmed = context.getResources()
105 .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
106 mPaddingBetweenElementsNormal = context.getResources()
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200107 .getDimensionPixelSize(R.dimen.notification_padding);
Selim Cinek67b22602014-03-10 15:40:16 +0100108 mCollapsedSize = context.getResources()
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200109 .getDimensionPixelSize(R.dimen.notification_min_height);
Selim Cinek3afd00e2014-08-11 22:32:57 +0200110 mMaxNotificationHeight = context.getResources()
111 .getDimensionPixelSize(R.dimen.notification_max_height);
Selim Cinek67b22602014-03-10 15:40:16 +0100112 mTopStackPeekSize = context.getResources()
113 .getDimensionPixelSize(R.dimen.top_stack_peek_amount);
114 mBottomStackPeekSize = context.getResources()
115 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
116 mZDistanceBetweenElements = context.getResources()
117 .getDimensionPixelSize(R.dimen.z_distance_between_notifications);
118 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200119 mBottomStackSlowDownLength = context.getResources()
120 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length);
Selim Cinekad3e5af2014-07-04 12:24:11 +0200121 mTopStackSlowDownLength = context.getResources()
122 .getDimensionPixelSize(R.dimen.top_stack_slow_down_length);
Selim Cinek708a6c12014-05-28 14:16:02 +0200123 mRoundedRectCornerRadius = context.getResources().getDimensionPixelSize(
Selim Cinek697178b2014-07-02 19:40:30 +0200124 R.dimen.notification_material_rounded_rect_radius);
Selim Cinekd83771e2014-07-04 16:45:31 +0200125 mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize(
126 R.dimen.notification_collapse_second_card_padding);
Jorim Jaggid7c1fae2014-08-13 18:27:47 +0200127 mScaleDimmed = context.getResources().getDisplayMetrics().densityDpi
128 >= DisplayMetrics.DENSITY_XXHIGH;
Selim Cinek67b22602014-03-10 15:40:16 +0100129 }
130
Jorim Jaggid7c1fae2014-08-13 18:27:47 +0200131 public boolean shouldScaleDimmed() {
132 return mScaleDimmed;
133 }
Selim Cinek67b22602014-03-10 15:40:16 +0100134
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200135 public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
Selim Cinek67b22602014-03-10 15:40:16 +0100136 // The state of the local variables are saved in an algorithmState to easily subdivide it
137 // into multiple phases.
138 StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
139
140 // First we reset the view states to their default values.
141 resultState.resetViewStates();
142
Selim Cinek343e6e22014-04-11 21:23:30 +0200143 algorithmState.itemsInTopStack = 0.0f;
Selim Cinek67b22602014-03-10 15:40:16 +0100144 algorithmState.partialInTop = 0.0f;
145 algorithmState.lastTopStackIndex = 0;
Selim Cinek343e6e22014-04-11 21:23:30 +0200146 algorithmState.scrolledPixelsTop = 0;
Selim Cinek67b22602014-03-10 15:40:16 +0100147 algorithmState.itemsInBottomStack = 0.0f;
Selim Cinek343e6e22014-04-11 21:23:30 +0200148 algorithmState.partialInBottom = 0.0f;
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200149 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
Selim Cinek1408eb52014-06-02 14:45:38 +0200150
151 int scrollY = ambientState.getScrollY();
152
153 // Due to the overScroller, the stackscroller can have negative scroll state. This is
154 // already accounted for by the top padding and doesn't need an additional adaption
155 scrollY = Math.max(0, scrollY);
156 algorithmState.scrollY = (int) (scrollY + mCollapsedSize + bottomOverScroll);
Selim Cinek343e6e22014-04-11 21:23:30 +0200157
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700158 updateVisibleChildren(resultState, algorithmState, ambientState);
Selim Cinek67b22602014-03-10 15:40:16 +0100159
160 // Phase 1:
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700161 findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState, ambientState);
Selim Cinek67b22602014-03-10 15:40:16 +0100162
163 // Phase 2:
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700164 updatePositionsForState(resultState, algorithmState, ambientState);
Selim Cinek67b22602014-03-10 15:40:16 +0100165
166 // Phase 3:
167 updateZValuesForState(resultState, algorithmState);
Selim Cinekeb973562014-05-02 17:07:49 +0200168
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200169 handleDraggedViews(ambientState, resultState, algorithmState);
Jorim Jaggiae441282014-08-01 02:45:18 +0200170 updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
Selim Cineka59ecc32015-04-07 10:51:49 -0700171 updateClipping(resultState, algorithmState, ambientState);
Selim Cinek3d2b94bf2014-07-02 22:12:47 +0200172 updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex());
Selim Cinekb5605e52015-02-20 18:21:41 +0100173 getNotificationChildrenStates(resultState, algorithmState);
174 }
175
176 private void getNotificationChildrenStates(StackScrollState resultState,
177 StackScrollAlgorithmState algorithmState) {
178 int childCount = algorithmState.visibleChildren.size();
179 for (int i = 0; i < childCount; i++) {
180 ExpandableView v = algorithmState.visibleChildren.get(i);
181 if (v instanceof ExpandableNotificationRow) {
182 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
183 row.getChildrenStates(resultState);
184 }
185 }
Selim Cinek3d2b94bf2014-07-02 22:12:47 +0200186 }
187
188 private void updateSpeedBumpState(StackScrollState resultState,
189 StackScrollAlgorithmState algorithmState, int speedBumpIndex) {
190 int childCount = algorithmState.visibleChildren.size();
191 for (int i = 0; i < childCount; i++) {
192 View child = algorithmState.visibleChildren.get(i);
Selim Cinekb036ca42015-02-20 15:56:28 +0100193 StackViewState childViewState = resultState.getViewStateForView(child);
Selim Cinek3107cfa2014-07-22 15:24:29 +0200194
195 // The speed bump can also be gone, so equality needs to be taken when comparing
196 // indices.
197 childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex;
Selim Cinek3d2b94bf2014-07-02 22:12:47 +0200198 }
Selim Cinekf54090e2014-06-17 17:24:51 -0700199 }
200
Selim Cinek708a6c12014-05-28 14:16:02 +0200201 private void updateClipping(StackScrollState resultState,
Selim Cineka59ecc32015-04-07 10:51:49 -0700202 StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
Selim Cinek708a6c12014-05-28 14:16:02 +0200203 float previousNotificationEnd = 0;
204 float previousNotificationStart = 0;
205 boolean previousNotificationIsSwiped = false;
206 int childCount = algorithmState.visibleChildren.size();
207 for (int i = 0; i < childCount; i++) {
208 ExpandableView child = algorithmState.visibleChildren.get(i);
Selim Cinekb036ca42015-02-20 15:56:28 +0100209 StackViewState state = resultState.getViewStateForView(child);
Jorim Jaggi2e34ec32014-07-01 03:17:05 +0200210 float newYTranslation = state.yTranslation + state.height * (1f - state.scale) / 2f;
211 float newHeight = state.height * state.scale;
Selim Cinek708a6c12014-05-28 14:16:02 +0200212 // apply clipping and shadow
213 float newNotificationEnd = newYTranslation + newHeight;
214
Selim Cinekc5baa3e2014-10-29 19:04:19 +0100215 float clipHeight;
216 if (previousNotificationIsSwiped) {
217 // When the previous notification is swiped, we don't clip the content to the
218 // bottom of it.
219 clipHeight = newHeight;
220 } else {
221 clipHeight = newNotificationEnd - previousNotificationEnd;
222 clipHeight = Math.max(0.0f, clipHeight);
223 if (clipHeight != 0.0f) {
Selim Cinek708a6c12014-05-28 14:16:02 +0200224
Selim Cinekc5baa3e2014-10-29 19:04:19 +0100225 // In the unlocked shade we have to clip a little bit higher because of the rounded
226 // corners of the notifications, but only if we are not fully overlapped by
227 // the top card.
228 float clippingCorrection = state.dimmed
229 ? 0
230 : mRoundedRectCornerRadius * state.scale;
231 clipHeight += clippingCorrection;
232 }
233 }
Selim Cinek708a6c12014-05-28 14:16:02 +0200234
235 updateChildClippingAndBackground(state, newHeight, clipHeight,
Jorim Jaggi2e34ec32014-07-01 03:17:05 +0200236 newHeight - (previousNotificationStart - newYTranslation));
Selim Cinek708a6c12014-05-28 14:16:02 +0200237
238 if (!child.isTransparent()) {
239 // Only update the previous values if we are not transparent,
240 // otherwise we would clip to a transparent view.
Jorim Jaggi2e34ec32014-07-01 03:17:05 +0200241 previousNotificationStart = newYTranslation + state.clipTopAmount * state.scale;
Selim Cinek708a6c12014-05-28 14:16:02 +0200242 previousNotificationEnd = newNotificationEnd;
Selim Cineka59ecc32015-04-07 10:51:49 -0700243 previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child);
Selim Cinek708a6c12014-05-28 14:16:02 +0200244 }
245 }
246 }
247
248 /**
249 * Updates the shadow outline and the clipping for a view.
250 *
251 * @param state the viewState to update
252 * @param realHeight the currently applied height of the view
253 * @param clipHeight the desired clip height, the rest of the view will be clipped from the top
254 * @param backgroundHeight the desired background height. The shadows of the view will be
255 * based on this height and the content will be clipped from the top
256 */
Selim Cinekb036ca42015-02-20 15:56:28 +0100257 private void updateChildClippingAndBackground(StackViewState state, float realHeight,
258 float clipHeight, float backgroundHeight) {
Selim Cinek708a6c12014-05-28 14:16:02 +0200259 if (realHeight > clipHeight) {
Jorim Jaggi2e34ec32014-07-01 03:17:05 +0200260 // Rather overlap than create a hole.
261 state.topOverLap = (int) Math.floor((realHeight - clipHeight) / state.scale);
Selim Cinek708a6c12014-05-28 14:16:02 +0200262 } else {
263 state.topOverLap = 0;
264 }
265 if (realHeight > backgroundHeight) {
Jorim Jaggi2e34ec32014-07-01 03:17:05 +0200266 // Rather overlap than create a hole.
267 state.clipTopAmount = (int) Math.floor((realHeight - backgroundHeight) / state.scale);
Selim Cinek708a6c12014-05-28 14:16:02 +0200268 } else {
269 state.clipTopAmount = 0;
270 }
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200271 }
272
273 /**
Jorim Jaggiae441282014-08-01 02:45:18 +0200274 * Updates the dimmed, activated and hiding sensitive states of the children.
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200275 */
Jorim Jaggiae441282014-08-01 02:45:18 +0200276 private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
277 StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200278 boolean dimmed = ambientState.isDimmed();
John Spurlockbf370992014-06-17 13:58:31 -0400279 boolean dark = ambientState.isDark();
Jorim Jaggiae441282014-08-01 02:45:18 +0200280 boolean hideSensitive = ambientState.isHideSensitive();
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200281 View activatedChild = ambientState.getActivatedChild();
282 int childCount = algorithmState.visibleChildren.size();
283 for (int i = 0; i < childCount; i++) {
284 View child = algorithmState.visibleChildren.get(i);
Selim Cinekb036ca42015-02-20 15:56:28 +0100285 StackViewState childViewState = resultState.getViewStateForView(child);
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200286 childViewState.dimmed = dimmed;
John Spurlockbf370992014-06-17 13:58:31 -0400287 childViewState.dark = dark;
Jorim Jaggiae441282014-08-01 02:45:18 +0200288 childViewState.hideSensitive = hideSensitive;
Selim Cinekb89de4e2014-06-10 10:47:05 +0200289 boolean isActivatedChild = activatedChild == child;
Jorim Jaggid7c1fae2014-08-13 18:27:47 +0200290 childViewState.scale = !mScaleDimmed || !dimmed || isActivatedChild
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200291 ? 1.0f
292 : DIMMED_SCALE;
Jorim Jaggi4538cee2014-09-09 15:21:38 +0200293 if (dimmed && isActivatedChild) {
294 childViewState.zTranslation += 2.0f * mZDistanceBetweenElements;
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200295 }
296 }
Selim Cinekeb973562014-05-02 17:07:49 +0200297 }
298
299 /**
300 * Handle the special state when views are being dragged
301 */
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200302 private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
Selim Cinekeb973562014-05-02 17:07:49 +0200303 StackScrollAlgorithmState algorithmState) {
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200304 ArrayList<View> draggedViews = ambientState.getDraggedViews();
305 for (View draggedView : draggedViews) {
Selim Cinekeb973562014-05-02 17:07:49 +0200306 int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
307 if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
308 View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200309 if (!draggedViews.contains(nextChild)) {
Selim Cinekeb973562014-05-02 17:07:49 +0200310 // only if the view is not dragged itself we modify its state to be fully
311 // visible
Selim Cinekb036ca42015-02-20 15:56:28 +0100312 StackViewState viewState = resultState.getViewStateForView(
Selim Cinekeb973562014-05-02 17:07:49 +0200313 nextChild);
314 // The child below the dragged one must be fully visible
Selim Cineka59ecc32015-04-07 10:51:49 -0700315 if (!isPinnedHeadsUpView(draggedView) || isPinnedHeadsUpView(nextChild)) {
316 viewState.alpha = 1;
317 }
Selim Cinekeb973562014-05-02 17:07:49 +0200318 }
319
320 // Lets set the alpha to the one it currently has, as its currently being dragged
Selim Cinekb036ca42015-02-20 15:56:28 +0100321 StackViewState viewState = resultState.getViewStateForView(draggedView);
Selim Cinekeb973562014-05-02 17:07:49 +0200322 // The dragged child should keep the set alpha
323 viewState.alpha = draggedView.getAlpha();
324 }
325 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200326 }
Selim Cinek67b22602014-03-10 15:40:16 +0100327
Selim Cineka59ecc32015-04-07 10:51:49 -0700328 private boolean isPinnedHeadsUpView(View view) {
329 if (view instanceof ExpandableNotificationRow) {
330 ExpandableNotificationRow row = (ExpandableNotificationRow) view;
331 return row.isHeadsUp() && !row.isInShade();
332 }
333 return false;
334 }
335
Selim Cinek343e6e22014-04-11 21:23:30 +0200336 /**
Jorim Jaggid4a57442014-04-10 02:45:55 +0200337 * Update the visible children on the state.
338 */
339 private void updateVisibleChildren(StackScrollState resultState,
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700340 StackScrollAlgorithmState state, AmbientState ambientState) {
Jorim Jaggid4a57442014-04-10 02:45:55 +0200341 ViewGroup hostView = resultState.getHostView();
342 int childCount = hostView.getChildCount();
343 state.visibleChildren.clear();
344 state.visibleChildren.ensureCapacity(childCount);
Selim Cinekb036ca42015-02-20 15:56:28 +0100345 int notGoneIndex = 0;
Selim Cineka59ecc32015-04-07 10:51:49 -0700346 TreeSet<HeadsUpManager.HeadsUpEntry> headsUpEntries
347 = ambientState.getSortedHeadsUpEntries();
348 for (HeadsUpManager.HeadsUpEntry entry: headsUpEntries) {
349 ExpandableView v = entry.entry.row;
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700350 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
351 }
Jorim Jaggid4a57442014-04-10 02:45:55 +0200352 for (int i = 0; i < childCount; i++) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200353 ExpandableView v = (ExpandableView) hostView.getChildAt(i);
Jorim Jaggid4a57442014-04-10 02:45:55 +0200354 if (v.getVisibility() != View.GONE) {
Selim Cinekb5605e52015-02-20 18:21:41 +0100355 if (v instanceof ExpandableNotificationRow) {
356 ExpandableNotificationRow row = (ExpandableNotificationRow) v;
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700357 if (row.isHeadsUp()) {
358 continue;
359 }
360 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
361
362 // handle the notgoneIndex for the children as well
Selim Cinekb5605e52015-02-20 18:21:41 +0100363 List<ExpandableNotificationRow> children =
364 row.getNotificationChildren();
365 if (row.areChildrenExpanded() && children != null) {
366 for (ExpandableNotificationRow childRow : children) {
367 if (childRow.getVisibility() != View.GONE) {
368 StackViewState childState
369 = resultState.getViewStateForView(childRow);
370 childState.notGoneIndex = notGoneIndex;
371 notGoneIndex++;
372 }
373 }
374 }
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700375 } else {
376 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
Selim Cinekb5605e52015-02-20 18:21:41 +0100377 }
Jorim Jaggid4a57442014-04-10 02:45:55 +0200378 }
379 }
380 }
381
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700382 private int updateNotGoneIndex(StackScrollState resultState,
383 StackScrollAlgorithmState state, int notGoneIndex,
384 ExpandableView v) {
385 StackViewState viewState = resultState.getViewStateForView(v);
386 viewState.notGoneIndex = notGoneIndex;
387 state.visibleChildren.add(v);
388 notGoneIndex++;
389 return notGoneIndex;
390 }
391
Jorim Jaggid4a57442014-04-10 02:45:55 +0200392 /**
Selim Cinek67b22602014-03-10 15:40:16 +0100393 * Determine the positions for the views. This is the main part of the algorithm.
394 *
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700395 * @param resultState The result state to update if a change to the properties of a child occurs
Selim Cinek67b22602014-03-10 15:40:16 +0100396 * @param algorithmState The state in which the current pass of the algorithm is currently in
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700397 * @param ambientState The current ambient state
Selim Cinek67b22602014-03-10 15:40:16 +0100398 */
399 private void updatePositionsForState(StackScrollState resultState,
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700400 StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
Selim Cinek67b22602014-03-10 15:40:16 +0100401
Selim Cinek1685e632014-04-08 02:27:49 +0200402 // The starting position of the bottom stack peek
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700403 float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize;
Selim Cinek1685e632014-04-08 02:27:49 +0200404
Selim Cinek67b22602014-03-10 15:40:16 +0100405 // The position where the bottom stack starts.
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200406 float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
Selim Cinek67b22602014-03-10 15:40:16 +0100407
408 // The y coordinate of the current child.
409 float currentYPosition = 0.0f;
410
411 // How far in is the element currently transitioning into the bottom stack.
412 float yPositionInScrollView = 0.0f;
413
Selim Cineka59ecc32015-04-07 10:51:49 -0700414 // If we have a heads-up higher than the collapsed height we need to add the difference to
415 // the padding of all other elements, i.e push in the top stack slightly.
416 ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry();
417
Jorim Jaggid4a57442014-04-10 02:45:55 +0200418 int childCount = algorithmState.visibleChildren.size();
Selim Cinek67b22602014-03-10 15:40:16 +0100419 int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
420 for (int i = 0; i < childCount; i++) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200421 ExpandableView child = algorithmState.visibleChildren.get(i);
Selim Cinekb036ca42015-02-20 15:56:28 +0100422 StackViewState childViewState = resultState.getViewStateForView(child);
423 childViewState.location = StackViewState.LOCATION_UNKNOWN;
Selim Cineka59ecc32015-04-07 10:51:49 -0700424 int childHeight = getMaxAllowedChildHeight(child, ambientState);
Selim Cinek67b22602014-03-10 15:40:16 +0100425 float yPositionInScrollViewAfterElement = yPositionInScrollView
426 + childHeight
427 + mPaddingBetweenElements;
Selim Cinek343e6e22014-04-11 21:23:30 +0200428 float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize;
429
430 if (i == algorithmState.lastTopStackIndex + 1) {
431 // Normally the position of this child is the position in the regular scrollview,
432 // but if the two stacks are very close to each other,
433 // then have have to push it even more upwards to the position of the bottom
434 // stack start.
435 currentYPosition = Math.min(scrollOffset, bottomStackStart);
436 }
437 childViewState.yTranslation = currentYPosition;
438
439 // The y position after this element
440 float nextYPosition = currentYPosition + childHeight +
441 mPaddingBetweenElements;
442
443 if (i <= algorithmState.lastTopStackIndex) {
Selim Cinek67b22602014-03-10 15:40:16 +0100444 // Case 1:
445 // We are in the top Stack
Selim Cinek343e6e22014-04-11 21:23:30 +0200446 updateStateForTopStackChild(algorithmState,
447 numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset);
Selim Cinek3afd00e2014-08-11 22:32:57 +0200448 clampPositionToTopStackEnd(childViewState, childHeight);
449
Selim Cinek343e6e22014-04-11 21:23:30 +0200450 // check if we are overlapping with the bottom stack
451 if (childViewState.yTranslation + childHeight + mPaddingBetweenElements
Selim Cinek4581cf82014-08-12 12:40:32 +0200452 >= bottomStackStart && !mIsExpansionChanging && i != 0 && mIsSmallScreen) {
Selim Cinek3afd00e2014-08-11 22:32:57 +0200453 // we just collapse this element slightly
454 int newSize = (int) Math.max(bottomStackStart - mPaddingBetweenElements -
455 childViewState.yTranslation, mCollapsedSize);
456 childViewState.height = newSize;
Selim Cinek4581cf82014-08-12 12:40:32 +0200457 updateStateForChildTransitioningInBottom(algorithmState, bottomStackStart,
458 bottomPeekStart, childViewState.yTranslation, childViewState,
459 childHeight);
Selim Cinek343e6e22014-04-11 21:23:30 +0200460 }
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700461 clampPositionToBottomStackStart(childViewState, childViewState.height,
462 ambientState);
Selim Cinek343e6e22014-04-11 21:23:30 +0200463 } else if (nextYPosition >= bottomStackStart) {
Selim Cinek67b22602014-03-10 15:40:16 +0100464 // Case 2:
Selim Cinek343e6e22014-04-11 21:23:30 +0200465 // We are in the bottom stack.
466 if (currentYPosition >= bottomStackStart) {
Selim Cinek67b22602014-03-10 15:40:16 +0100467 // According to the regular scroll view we are fully translated out of the
468 // bottom of the screen so we are fully in the bottom stack
Selim Cinek343e6e22014-04-11 21:23:30 +0200469 updateStateForChildFullyInBottomStack(algorithmState,
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700470 bottomStackStart, childViewState, childHeight, ambientState);
Selim Cinek67b22602014-03-10 15:40:16 +0100471 } else {
Selim Cinek67b22602014-03-10 15:40:16 +0100472 // According to the regular scroll view we are currently translating out of /
473 // into the bottom of the screen
Selim Cinek343e6e22014-04-11 21:23:30 +0200474 updateStateForChildTransitioningInBottom(algorithmState,
475 bottomStackStart, bottomPeekStart, currentYPosition,
476 childViewState, childHeight);
Selim Cinek67b22602014-03-10 15:40:16 +0100477 }
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200478 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200479 // Case 3:
480 // We are in the regular scroll area.
Selim Cinekb036ca42015-02-20 15:56:28 +0100481 childViewState.location = StackViewState.LOCATION_MAIN_AREA;
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700482 clampYTranslation(childViewState, childHeight, ambientState);
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200483 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200484
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200485 // The first card is always rendered.
486 if (i == 0) {
487 childViewState.alpha = 1.0f;
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200488 childViewState.yTranslation = Math.max(mCollapsedSize - algorithmState.scrollY, 0);
Selim Cinekd83771e2014-07-04 16:45:31 +0200489 if (childViewState.yTranslation + childViewState.height
490 > bottomPeekStart - mCollapseSecondCardPadding) {
Selim Cinek4fe3e472014-07-03 16:32:54 +0200491 childViewState.height = (int) Math.max(
Selim Cinekd83771e2014-07-04 16:45:31 +0200492 bottomPeekStart - mCollapseSecondCardPadding
493 - childViewState.yTranslation, mCollapsedSize);
Selim Cinek4fe3e472014-07-03 16:32:54 +0200494 }
Selim Cinekb036ca42015-02-20 15:56:28 +0100495 childViewState.location = StackViewState.LOCATION_FIRST_CARD;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200496 }
Selim Cinekb036ca42015-02-20 15:56:28 +0100497 if (childViewState.location == StackViewState.LOCATION_UNKNOWN) {
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200498 Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
Selim Cinek67b22602014-03-10 15:40:16 +0100499 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200500 currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
Selim Cinek67b22602014-03-10 15:40:16 +0100501 yPositionInScrollView = yPositionInScrollViewAfterElement;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200502
Selim Cineka59ecc32015-04-07 10:51:49 -0700503 if (ambientState.isShadeExpanded() && topHeadsUpEntry != null
504 && child != topHeadsUpEntry) {
505 childViewState.yTranslation += topHeadsUpEntry.getHeadsUpHeight() - mCollapsedSize;
506 }
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700507 childViewState.yTranslation += ambientState.getTopPadding()
Selim Cineka59ecc32015-04-07 10:51:49 -0700508 + ambientState.getStackTranslation();
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700509 }
Selim Cinek8d490d42015-04-10 00:05:50 -0700510 updateHeadsUpStates(resultState, ambientState);
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700511 }
512
Selim Cinek8d490d42015-04-10 00:05:50 -0700513 private void updateHeadsUpStates(StackScrollState resultState, AmbientState ambientState) {
Selim Cineka59ecc32015-04-07 10:51:49 -0700514 TreeSet<HeadsUpManager.HeadsUpEntry> headsUpEntries = ambientState.getSortedHeadsUpEntries();
515 for (HeadsUpManager.HeadsUpEntry entry: headsUpEntries) {
516 ExpandableNotificationRow row = entry.entry.row;
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700517 StackViewState childState = resultState.getViewStateForView(row);
Selim Cinek1f3f5442015-04-10 17:54:46 -0700518 ExpandableNotificationRow topHeadsUpEntry = ambientState.getTopHeadsUpEntry();
519 boolean isTopEntry = topHeadsUpEntry == row;
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700520 if (!row.isInShade()) {
Selim Cineka59ecc32015-04-07 10:51:49 -0700521 childState.yTranslation = 0;
Selim Cinek1f3f5442015-04-10 17:54:46 -0700522 childState.height = row.getHeadsUpHeight();
523 if (!isTopEntry) {
524 // Ensure that a headsUp is never below the topmost headsUp
525 StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
526 childState.height = row.getHeadsUpHeight();
Selim Cineke53e6bb2015-04-13 16:14:26 -0700527 childState.yTranslation = topState.yTranslation + topState.height
528 - childState.height;
Selim Cinek1f3f5442015-04-10 17:54:46 -0700529 }
530 } else if (mIsExpanded) {
531 if (isTopEntry) {
532 childState.height += row.getHeadsUpHeight() - mCollapsedSize;
533 }
534 childState.height = Math.max(childState.height, row.getHeadsUpHeight());
535 // Ensure that the heads up is always visible even when scrolled of from the bottom
536 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - childState.height;
537 childState.yTranslation = Math.min(childState.yTranslation,
538 bottomPosition);
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700539 }
Selim Cineka59ecc32015-04-07 10:51:49 -0700540
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700541 }
Selim Cinek67b22602014-03-10 15:40:16 +0100542 }
543
Selim Cinek1685e632014-04-08 02:27:49 +0200544 /**
Selim Cinek343e6e22014-04-11 21:23:30 +0200545 * Clamp the yTranslation both up and down to valid positions.
Selim Cinek1685e632014-04-08 02:27:49 +0200546 *
Selim Cinek1685e632014-04-08 02:27:49 +0200547 * @param childViewState the view state of the child
Selim Cinek343e6e22014-04-11 21:23:30 +0200548 * @param childHeight the height of this child
Selim Cinek1685e632014-04-08 02:27:49 +0200549 */
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700550 private void clampYTranslation(StackViewState childViewState, int childHeight,
551 AmbientState ambientState) {
552 clampPositionToBottomStackStart(childViewState, childHeight, ambientState);
Selim Cinek343e6e22014-04-11 21:23:30 +0200553 clampPositionToTopStackEnd(childViewState, childHeight);
554 }
555
556 /**
557 * Clamp the yTranslation of the child down such that its end is at most on the beginning of
558 * the bottom stack.
559 *
560 * @param childViewState the view state of the child
561 * @param childHeight the height of this child
562 */
Selim Cinekb036ca42015-02-20 15:56:28 +0100563 private void clampPositionToBottomStackStart(StackViewState childViewState,
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700564 int childHeight, AmbientState ambientState) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200565 childViewState.yTranslation = Math.min(childViewState.yTranslation,
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700566 ambientState.getInnerHeight() - mBottomStackPeekSize - mCollapseSecondCardPadding
567 - childHeight);
Selim Cinek343e6e22014-04-11 21:23:30 +0200568 }
569
570 /**
571 * Clamp the yTranslation of the child up such that its end is at lest on the end of the top
Selim Cineka59ecc32015-04-07 10:51:49 -0700572 * stack.
Selim Cinek343e6e22014-04-11 21:23:30 +0200573 *
574 * @param childViewState the view state of the child
575 * @param childHeight the height of this child
576 */
Selim Cinekb036ca42015-02-20 15:56:28 +0100577 private void clampPositionToTopStackEnd(StackViewState childViewState,
Selim Cinek343e6e22014-04-11 21:23:30 +0200578 int childHeight) {
579 childViewState.yTranslation = Math.max(childViewState.yTranslation,
580 mCollapsedSize - childHeight);
Selim Cinek1685e632014-04-08 02:27:49 +0200581 }
582
Selim Cineka59ecc32015-04-07 10:51:49 -0700583 private int getMaxAllowedChildHeight(View child, AmbientState ambientState) {
Selim Cinek1685e632014-04-08 02:27:49 +0200584 if (child instanceof ExpandableNotificationRow) {
585 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
Selim Cinekd2281152015-04-10 14:37:46 -0700586 if (ambientState == null && row.isHeadsUp()
587 || ambientState != null && ambientState.getTopHeadsUpEntry() == child) {
Selim Cinek8d490d42015-04-10 00:05:50 -0700588 int extraSize = row.getIntrinsicHeight() - row.getHeadsUpHeight();
589 return mCollapsedSize + extraSize;
Selim Cineka59ecc32015-04-07 10:51:49 -0700590 }
Jorim Jaggi9cbadd32014-05-01 20:18:31 +0200591 return row.getIntrinsicHeight();
Jorim Jaggibe565df2014-04-28 17:51:23 +0200592 } else if (child instanceof ExpandableView) {
593 ExpandableView expandableView = (ExpandableView) child;
594 return expandableView.getActualHeight();
Selim Cinek1685e632014-04-08 02:27:49 +0200595 }
Jorim Jaggibe565df2014-04-28 17:51:23 +0200596 return child == null? mCollapsedSize : child.getHeight();
Selim Cinek1685e632014-04-08 02:27:49 +0200597 }
598
Selim Cinek343e6e22014-04-11 21:23:30 +0200599 private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
600 float transitioningPositionStart, float bottomPeakStart, float currentYPosition,
Selim Cinekb036ca42015-02-20 15:56:28 +0100601 StackViewState childViewState, int childHeight) {
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200602
Selim Cinek343e6e22014-04-11 21:23:30 +0200603 // This is the transitioning element on top of bottom stack, calculate how far we are in.
604 algorithmState.partialInBottom = 1.0f - (
605 (transitioningPositionStart - currentYPosition) / (childHeight +
606 mPaddingBetweenElements));
607
608 // the offset starting at the transitionPosition of the bottom stack
609 float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom);
610 algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
Selim Cinek3afd00e2014-08-11 22:32:57 +0200611 int newHeight = childHeight;
612 if (childHeight > mCollapsedSize && mIsSmallScreen) {
613 newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset -
614 mPaddingBetweenElements - currentYPosition, childHeight), mCollapsedSize);
615 childViewState.height = newHeight;
616 }
617 childViewState.yTranslation = transitioningPositionStart + offset - newHeight
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200618 - mPaddingBetweenElements;
Selim Cinek3afd00e2014-08-11 22:32:57 +0200619
Selim Cinek343e6e22014-04-11 21:23:30 +0200620 // We want at least to be at the end of the top stack when collapsing
Selim Cinek3afd00e2014-08-11 22:32:57 +0200621 clampPositionToTopStackEnd(childViewState, newHeight);
Selim Cinekb036ca42015-02-20 15:56:28 +0100622 childViewState.location = StackViewState.LOCATION_MAIN_AREA;
Selim Cinek67b22602014-03-10 15:40:16 +0100623 }
624
Selim Cinek343e6e22014-04-11 21:23:30 +0200625 private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
Selim Cinekb036ca42015-02-20 15:56:28 +0100626 float transitioningPositionStart, StackViewState childViewState,
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700627 int childHeight, AmbientState ambientState) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200628 float currentYPosition;
Selim Cinek67b22602014-03-10 15:40:16 +0100629 algorithmState.itemsInBottomStack += 1.0f;
630 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
631 // We are visually entering the bottom stack
Selim Cinek343e6e22014-04-11 21:23:30 +0200632 currentYPosition = transitioningPositionStart
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200633 + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack)
634 - mPaddingBetweenElements;
Selim Cinekb036ca42015-02-20 15:56:28 +0100635 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING;
Selim Cinek67b22602014-03-10 15:40:16 +0100636 } else {
637 // we are fully inside the stack
638 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
639 childViewState.alpha = 0.0f;
640 } else if (algorithmState.itemsInBottomStack
641 > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
642 childViewState.alpha = 1.0f - algorithmState.partialInBottom;
643 }
Selim Cinekb036ca42015-02-20 15:56:28 +0100644 childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN;
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700645 currentYPosition = ambientState.getInnerHeight();
Selim Cinek67b22602014-03-10 15:40:16 +0100646 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200647 childViewState.yTranslation = currentYPosition - childHeight;
648 clampPositionToTopStackEnd(childViewState, childHeight);
Selim Cinek67b22602014-03-10 15:40:16 +0100649 }
650
Selim Cinek343e6e22014-04-11 21:23:30 +0200651 private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
652 int numberOfElementsCompletelyIn, int i, int childHeight,
Selim Cinekb036ca42015-02-20 15:56:28 +0100653 StackViewState childViewState, float scrollOffset) {
Selim Cinek67b22602014-03-10 15:40:16 +0100654
Selim Cinek67b22602014-03-10 15:40:16 +0100655
656 // First we calculate the index relative to the current stack window of size at most
657 // {@link #MAX_ITEMS_IN_TOP_STACK}
Selim Cinek343e6e22014-04-11 21:23:30 +0200658 int paddedIndex = i - 1
Selim Cinek67b22602014-03-10 15:40:16 +0100659 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
660 if (paddedIndex >= 0) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200661
Selim Cinek67b22602014-03-10 15:40:16 +0100662 // We are currently visually entering the top stack
Selim Cinekad3e5af2014-07-04 12:24:11 +0200663 float distanceToStack = (childHeight + mPaddingBetweenElements)
664 - algorithmState.scrolledPixelsTop;
665 if (i == algorithmState.lastTopStackIndex
666 && distanceToStack > (mTopStackTotalSize + mPaddingBetweenElements)) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200667
668 // Child is currently translating into stack but not yet inside slow down zone.
669 // Handle it like the regular scrollview.
670 childViewState.yTranslation = scrollOffset;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200671 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200672 // Apply stacking logic.
673 float numItemsBefore;
674 if (i == algorithmState.lastTopStackIndex) {
Selim Cinekad3e5af2014-07-04 12:24:11 +0200675 numItemsBefore = 1.0f
676 - (distanceToStack / (mTopStackTotalSize + mPaddingBetweenElements));
Selim Cinek343e6e22014-04-11 21:23:30 +0200677 } else {
678 numItemsBefore = algorithmState.itemsInTopStack - i;
679 }
680 // The end position of the current child
Selim Cinekad3e5af2014-07-04 12:24:11 +0200681 float currentChildEndY = mCollapsedSize + mTopStackTotalSize
682 - mTopStackIndentationFunctor.getValue(numItemsBefore);
Selim Cinek343e6e22014-04-11 21:23:30 +0200683 childViewState.yTranslation = currentChildEndY - childHeight;
Selim Cinek67b22602014-03-10 15:40:16 +0100684 }
Selim Cinekb036ca42015-02-20 15:56:28 +0100685 childViewState.location = StackViewState.LOCATION_TOP_STACK_PEEKING;
Selim Cinek67b22602014-03-10 15:40:16 +0100686 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200687 if (paddedIndex == -1) {
688 childViewState.alpha = 1.0f - algorithmState.partialInTop;
689 } else {
690 // We are hidden behind the top card and faded out, so we can hide ourselves.
691 childViewState.alpha = 0.0f;
692 }
693 childViewState.yTranslation = mCollapsedSize - childHeight;
Selim Cinekb036ca42015-02-20 15:56:28 +0100694 childViewState.location = StackViewState.LOCATION_TOP_STACK_HIDDEN;
Selim Cinek67b22602014-03-10 15:40:16 +0100695 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200696
697
Selim Cinek67b22602014-03-10 15:40:16 +0100698 }
699
700 /**
701 * Find the number of items in the top stack and update the result state if needed.
702 *
703 * @param resultState The result state to update if a height change of an child occurs
704 * @param algorithmState The state in which the current pass of the algorithm is currently in
Selim Cinek67b22602014-03-10 15:40:16 +0100705 */
706 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700707 StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
Selim Cinek67b22602014-03-10 15:40:16 +0100708
709 // The y Position if the element would be in a regular scrollView
710 float yPositionInScrollView = 0.0f;
Jorim Jaggid4a57442014-04-10 02:45:55 +0200711 int childCount = algorithmState.visibleChildren.size();
Selim Cinek67b22602014-03-10 15:40:16 +0100712
713 // find the number of elements in the top stack.
714 for (int i = 0; i < childCount; i++) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200715 ExpandableView child = algorithmState.visibleChildren.get(i);
Selim Cinekb036ca42015-02-20 15:56:28 +0100716 StackViewState childViewState = resultState.getViewStateForView(child);
Selim Cineka59ecc32015-04-07 10:51:49 -0700717 int childHeight = getMaxAllowedChildHeight(child, ambientState);
Selim Cinek67b22602014-03-10 15:40:16 +0100718 float yPositionInScrollViewAfterElement = yPositionInScrollView
719 + childHeight
720 + mPaddingBetweenElements;
721 if (yPositionInScrollView < algorithmState.scrollY) {
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200722 if (i == 0 && algorithmState.scrollY <= mCollapsedSize) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200723
724 // The starting position of the bottom stack peek
Selim Cinekb8f09cf2015-03-16 17:09:28 -0700725 int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize -
Selim Cinekd83771e2014-07-04 16:45:31 +0200726 mCollapseSecondCardPadding;
Selim Cinek343e6e22014-04-11 21:23:30 +0200727 // Collapse and expand the first child while the shade is being expanded
728 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
729 ? mFirstChildMaxHeight
730 : childHeight;
731 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight),
732 mCollapsedSize);
733 algorithmState.itemsInTopStack = 1.0f;
734
735 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) {
Selim Cinek67b22602014-03-10 15:40:16 +0100736 // According to the regular scroll view we are fully off screen
737 algorithmState.itemsInTopStack += 1.0f;
Selim Cinek343e6e22014-04-11 21:23:30 +0200738 if (i == 0) {
Selim Cinek67b22602014-03-10 15:40:16 +0100739 childViewState.height = mCollapsedSize;
740 }
741 } else {
742 // According to the regular scroll view we are partially off screen
Selim Cinek343e6e22014-04-11 21:23:30 +0200743
Selim Cinek67b22602014-03-10 15:40:16 +0100744 // How much did we scroll into this child
Selim Cinekad3e5af2014-07-04 12:24:11 +0200745 algorithmState.scrolledPixelsTop = algorithmState.scrollY
746 - yPositionInScrollView;
Selim Cinek343e6e22014-04-11 21:23:30 +0200747 algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight
Selim Cinek67b22602014-03-10 15:40:16 +0100748 + mPaddingBetweenElements);
749
750 // Our element can be expanded, so this can get negative
751 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
752 algorithmState.itemsInTopStack += algorithmState.partialInTop;
Selim Cinekad3e5af2014-07-04 12:24:11 +0200753
Selim Cinek343e6e22014-04-11 21:23:30 +0200754 if (i == 0) {
Selim Cinekad3e5af2014-07-04 12:24:11 +0200755 // If it is expanded we have to collapse it to a new size
756 float newSize = yPositionInScrollViewAfterElement
757 - mPaddingBetweenElements
758 - algorithmState.scrollY + mCollapsedSize;
759 newSize = Math.max(mCollapsedSize, newSize);
Selim Cinek4e456be2014-06-12 18:09:43 +0200760 algorithmState.itemsInTopStack = 1.0f;
Selim Cinek67b22602014-03-10 15:40:16 +0100761 childViewState.height = (int) newSize;
Selim Cinek67b22602014-03-10 15:40:16 +0100762 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200763 algorithmState.lastTopStackIndex = i;
764 break;
Selim Cinek67b22602014-03-10 15:40:16 +0100765 }
766 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200767 algorithmState.lastTopStackIndex = i - 1;
Selim Cinek67b22602014-03-10 15:40:16 +0100768 // We are already past the stack so we can end the loop
769 break;
770 }
771 yPositionInScrollView = yPositionInScrollViewAfterElement;
772 }
773 }
774
775 /**
776 * Calculate the Z positions for all children based on the number of items in both stacks and
777 * save it in the resultState
778 *
779 * @param resultState The result state to update the zTranslation values
780 * @param algorithmState The state in which the current pass of the algorithm is currently in
781 */
782 private void updateZValuesForState(StackScrollState resultState,
783 StackScrollAlgorithmState algorithmState) {
Jorim Jaggid4a57442014-04-10 02:45:55 +0200784 int childCount = algorithmState.visibleChildren.size();
Selim Cinek67b22602014-03-10 15:40:16 +0100785 for (int i = 0; i < childCount; i++) {
Jorim Jaggid4a57442014-04-10 02:45:55 +0200786 View child = algorithmState.visibleChildren.get(i);
Selim Cinekb036ca42015-02-20 15:56:28 +0100787 StackViewState childViewState = resultState.getViewStateForView(child);
Selim Cinek67b22602014-03-10 15:40:16 +0100788 if (i < algorithmState.itemsInTopStack) {
789 float stackIndex = algorithmState.itemsInTopStack - i;
Selim Cinekc5baa3e2014-10-29 19:04:19 +0100790
791 // Ensure that the topmost item is a little bit higher than the rest when fully
792 // scrolled, to avoid drawing errors when swiping it out
793 float max = MAX_ITEMS_IN_TOP_STACK + (i == 0 ? 2.5f : 2);
794 stackIndex = Math.min(stackIndex, max);
Selim Cinek4e456be2014-06-12 18:09:43 +0200795 if (i == 0 && algorithmState.itemsInTopStack < 2.0f) {
796
797 // We only have the top item and an additional item in the top stack,
798 // Interpolate the index from 0 to 2 while the second item is
799 // translating in.
800 stackIndex -= 1.0f;
801 if (algorithmState.scrollY > mCollapsedSize) {
802
803 // Since there is a shadow treshhold, we cant just interpolate from 0 to
804 // 2 but we interpolate from 0.1f to 2.0f when scrolled in. The jump in
805 // height will not be noticable since we have padding in between.
806 stackIndex = 0.1f + stackIndex * 1.9f;
807 }
808 }
Selim Cinek67b22602014-03-10 15:40:16 +0100809 childViewState.zTranslation = mZBasicHeight
810 + stackIndex * mZDistanceBetweenElements;
811 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
812 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
813 float translationZ = mZBasicHeight
814 - numItemsAbove * mZDistanceBetweenElements;
815 childViewState.zTranslation = translationZ;
816 } else {
817 childViewState.zTranslation = mZBasicHeight;
818 }
819 }
820 }
821
Selim Cinek3afd00e2014-08-11 22:32:57 +0200822 /**
823 * Update whether the device is very small, i.e. Notifications can be in both the top and the
824 * bottom stack at the same time
825 *
826 * @param panelHeight The normal height of the panel when it's open
827 */
828 public void updateIsSmallScreen(int panelHeight) {
829 mIsSmallScreen = panelHeight <
830 mCollapsedSize /* top stack */
831 + mBottomStackSlowDownLength + mBottomStackPeekSize /* bottom stack */
832 + mMaxNotificationHeight; /* max notification height */
833 }
834
Selim Cinek1685e632014-04-08 02:27:49 +0200835 public void onExpansionStarted(StackScrollState currentState) {
836 mIsExpansionChanging = true;
837 mExpandedOnStart = mIsExpanded;
838 ViewGroup hostView = currentState.getHostView();
839 updateFirstChildHeightWhileExpanding(hostView);
840 }
841
842 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200843 mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView);
Jorim Jaggi9f347ae2014-04-11 00:47:18 +0200844 if (mFirstChildWhileExpanding != null) {
Selim Cinek1685e632014-04-08 02:27:49 +0200845 if (mExpandedOnStart) {
846
847 // We are collapsing the shade, so the first child can get as most as high as the
Selim Cinek02af41e2014-10-14 15:46:43 +0200848 // current height or the end value of the animation.
849 mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight(
850 mFirstChildWhileExpanding);
Selim Cinekd2281152015-04-10 14:37:46 -0700851 if (mFirstChildWhileExpanding instanceof ExpandableNotificationRow) {
852 ExpandableNotificationRow row =
853 (ExpandableNotificationRow) mFirstChildWhileExpanding;
854 if (row.isHeadsUp()) {
855 mFirstChildMaxHeight += mCollapsedSize - row.getHeadsUpHeight();
856 }
857 }
Selim Cinek1685e632014-04-08 02:27:49 +0200858 } else {
Selim Cinek31094df2014-08-14 19:28:15 +0200859 updateFirstChildMaxSizeToMaxHeight();
Selim Cinek1685e632014-04-08 02:27:49 +0200860 }
861 } else {
Selim Cinek1685e632014-04-08 02:27:49 +0200862 mFirstChildMaxHeight = 0;
863 }
864 }
865
Selim Cinek31094df2014-08-14 19:28:15 +0200866 private void updateFirstChildMaxSizeToMaxHeight() {
867 // We are expanding the shade, expand it to its full height.
868 if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) {
869
870 // This child was not layouted yet, wait for a layout pass
871 mFirstChildWhileExpanding
872 .addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
873 @Override
874 public void onLayoutChange(View v, int left, int top, int right,
875 int bottom, int oldLeft, int oldTop, int oldRight,
876 int oldBottom) {
877 if (mFirstChildWhileExpanding != null) {
878 mFirstChildMaxHeight = getMaxAllowedChildHeight(
Selim Cineka59ecc32015-04-07 10:51:49 -0700879 mFirstChildWhileExpanding, null);
Selim Cinek31094df2014-08-14 19:28:15 +0200880 } else {
881 mFirstChildMaxHeight = 0;
882 }
883 v.removeOnLayoutChangeListener(this);
884 }
885 });
886 } else {
Selim Cineka59ecc32015-04-07 10:51:49 -0700887 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding, null);
Selim Cinek31094df2014-08-14 19:28:15 +0200888 }
889 }
890
Selim Cinek7d447722014-06-10 15:51:59 +0200891 private boolean isMaxSizeInitialized(ExpandableView child) {
892 if (child instanceof ExpandableNotificationRow) {
893 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
Selim Cinek31094df2014-08-14 19:28:15 +0200894 return row.isMaxExpandHeightInitialized();
Selim Cinek7d447722014-06-10 15:51:59 +0200895 }
896 return child == null || child.getWidth() != 0;
897 }
898
Jorim Jaggi9f347ae2014-04-11 00:47:18 +0200899 private View findFirstVisibleChild(ViewGroup container) {
Selim Cinekd2281152015-04-10 14:37:46 -0700900 if (mHeadsUpManager != null && mHeadsUpManager.getTopEntry() != null) {
901 return mHeadsUpManager.getTopEntry().entry.row;
902 }
Jorim Jaggi9f347ae2014-04-11 00:47:18 +0200903 int childCount = container.getChildCount();
904 for (int i = 0; i < childCount; i++) {
905 View child = container.getChildAt(i);
906 if (child.getVisibility() != View.GONE) {
907 return child;
908 }
909 }
910 return null;
911 }
912
Selim Cinek1685e632014-04-08 02:27:49 +0200913 public void onExpansionStopped() {
914 mIsExpansionChanging = false;
915 mFirstChildWhileExpanding = null;
916 }
917
918 public void setIsExpanded(boolean isExpanded) {
919 this.mIsExpanded = isExpanded;
920 }
921
Selim Cinek02af41e2014-10-14 15:46:43 +0200922 public void notifyChildrenChanged(final ViewGroup hostView) {
Selim Cinek1685e632014-04-08 02:27:49 +0200923 if (mIsExpansionChanging) {
Selim Cinek02af41e2014-10-14 15:46:43 +0200924 hostView.post(new Runnable() {
925 @Override
926 public void run() {
927 updateFirstChildHeightWhileExpanding(hostView);
928 }
929 });
Selim Cinek1685e632014-04-08 02:27:49 +0200930 }
931 }
932
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200933 public void setDimmed(boolean dimmed) {
934 updatePadding(dimmed);
935 }
936
Selim Cinek31094df2014-08-14 19:28:15 +0200937 public void onReset(ExpandableView view) {
938 if (view.equals(mFirstChildWhileExpanding)) {
939 updateFirstChildMaxSizeToMaxHeight();
940 }
941 }
942
Selim Cinekd2281152015-04-10 14:37:46 -0700943 public void setHeadsUpManager(HeadsUpManager headsUpManager) {
944 mHeadsUpManager = headsUpManager;
945 }
946
Selim Cinek67b22602014-03-10 15:40:16 +0100947 class StackScrollAlgorithmState {
948
949 /**
950 * The scroll position of the algorithm
951 */
952 public int scrollY;
953
954 /**
955 * The quantity of items which are in the top stack.
956 */
957 public float itemsInTopStack;
958
959 /**
960 * how far in is the element currently transitioning into the top stack
961 */
962 public float partialInTop;
963
964 /**
Selim Cinek343e6e22014-04-11 21:23:30 +0200965 * The number of pixels the last child in the top stack has scrolled in to the stack
966 */
967 public float scrolledPixelsTop;
968
969 /**
Selim Cinek67b22602014-03-10 15:40:16 +0100970 * The last item index which is in the top stack.
Selim Cinek67b22602014-03-10 15:40:16 +0100971 */
972 public int lastTopStackIndex;
973
974 /**
975 * The quantity of items which are in the bottom stack.
976 */
977 public float itemsInBottomStack;
978
979 /**
980 * how far in is the element currently transitioning into the bottom stack
981 */
982 public float partialInBottom;
Jorim Jaggid4a57442014-04-10 02:45:55 +0200983
984 /**
985 * The children from the host view which are not gone.
986 */
Jorim Jaggibe565df2014-04-28 17:51:23 +0200987 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
Selim Cinek67b22602014-03-10 15:40:16 +0100988 }
989
990}