blob: bd9de8270c68594b908678ccd2e435b08a957e8f [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;
Christoph Studer6e3eceb2014-04-01 18:40:27 +020020import android.util.Log;
Selim Cinek67b22602014-03-10 15:40:16 +010021import android.view.View;
22import android.view.ViewGroup;
Selim Cinek343e6e22014-04-11 21:23:30 +020023
Selim Cinek67b22602014-03-10 15:40:16 +010024import com.android.systemui.R;
Selim Cinek1685e632014-04-08 02:27:49 +020025import com.android.systemui.statusbar.ExpandableNotificationRow;
Jorim Jaggibe565df2014-04-28 17:51:23 +020026import com.android.systemui.statusbar.ExpandableView;
Selim Cinek67b22602014-03-10 15:40:16 +010027
Jorim Jaggid4a57442014-04-10 02:45:55 +020028import java.util.ArrayList;
29
Selim Cinek67b22602014-03-10 15:40:16 +010030/**
31 * The Algorithm of the {@link com.android.systemui.statusbar.stack
32 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
33 * .stack.StackScrollState}
34 */
35public class StackScrollAlgorithm {
36
Christoph Studer6e3eceb2014-04-01 18:40:27 +020037 private static final String LOG_TAG = "StackScrollAlgorithm";
38
Selim Cinek67b22602014-03-10 15:40:16 +010039 private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
40 private static final int MAX_ITEMS_IN_TOP_STACK = 3;
41
Jorim Jaggid552d9d2014-05-07 19:41:13 +020042 /** When a child is activated, the other cards' alpha fade to this value. */
43 private static final float ACTIVATED_INVERSE_ALPHA = 0.9f;
44 private static final float DIMMED_SCALE = 0.95f;
45
Selim Cinek67b22602014-03-10 15:40:16 +010046 private int mPaddingBetweenElements;
47 private int mCollapsedSize;
48 private int mTopStackPeekSize;
49 private int mBottomStackPeekSize;
50 private int mZDistanceBetweenElements;
51 private int mZBasicHeight;
52
53 private StackIndentationFunctor mTopStackIndentationFunctor;
54 private StackIndentationFunctor mBottomStackIndentationFunctor;
55
Selim Cinek343e6e22014-04-11 21:23:30 +020056 private int mLayoutHeight;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +020057
58 /** mLayoutHeight - mTopPadding */
59 private int mInnerHeight;
60 private int mTopPadding;
Selim Cinek67b22602014-03-10 15:40:16 +010061 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
Selim Cinek1685e632014-04-08 02:27:49 +020062 private boolean mIsExpansionChanging;
63 private int mFirstChildMaxHeight;
64 private boolean mIsExpanded;
Jorim Jaggibe565df2014-04-28 17:51:23 +020065 private ExpandableView mFirstChildWhileExpanding;
Selim Cinek1685e632014-04-08 02:27:49 +020066 private boolean mExpandedOnStart;
Selim Cinek343e6e22014-04-11 21:23:30 +020067 private int mTopStackTotalSize;
Selim Cinek34c0a8d2014-05-12 00:01:43 +020068 private int mPaddingBetweenElementsDimmed;
69 private int mPaddingBetweenElementsNormal;
70 private int mBottomStackSlowDownLength;
Selim Cinek67b22602014-03-10 15:40:16 +010071
72 public StackScrollAlgorithm(Context context) {
73 initConstants(context);
Selim Cinek34c0a8d2014-05-12 00:01:43 +020074 updatePadding(false);
75 }
76
77 private void updatePadding(boolean dimmed) {
78 mPaddingBetweenElements = dimmed
79 ? mPaddingBetweenElementsDimmed
80 : mPaddingBetweenElementsNormal;
81 mTopStackTotalSize = mCollapsedSize + mPaddingBetweenElements;
82 mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
83 MAX_ITEMS_IN_TOP_STACK,
84 mTopStackPeekSize,
Selim Cinekb96924d2014-05-12 15:11:25 +020085 mTopStackTotalSize - mTopStackPeekSize,
Selim Cinek34c0a8d2014-05-12 00:01:43 +020086 0.5f);
87 mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
88 MAX_ITEMS_IN_BOTTOM_STACK,
89 mBottomStackPeekSize,
90 getBottomStackSlowDownLength(),
91 0.5f);
92 }
93
94 public int getBottomStackSlowDownLength() {
95 return mBottomStackSlowDownLength + mPaddingBetweenElements;
Selim Cinek67b22602014-03-10 15:40:16 +010096 }
97
98 private void initConstants(Context context) {
Selim Cinek34c0a8d2014-05-12 00:01:43 +020099 mPaddingBetweenElementsDimmed = context.getResources()
100 .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
101 mPaddingBetweenElementsNormal = context.getResources()
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200102 .getDimensionPixelSize(R.dimen.notification_padding);
Selim Cinek67b22602014-03-10 15:40:16 +0100103 mCollapsedSize = context.getResources()
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200104 .getDimensionPixelSize(R.dimen.notification_min_height);
Selim Cinek67b22602014-03-10 15:40:16 +0100105 mTopStackPeekSize = context.getResources()
106 .getDimensionPixelSize(R.dimen.top_stack_peek_amount);
107 mBottomStackPeekSize = context.getResources()
108 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
109 mZDistanceBetweenElements = context.getResources()
110 .getDimensionPixelSize(R.dimen.z_distance_between_notifications);
111 mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200112 mBottomStackSlowDownLength = context.getResources()
113 .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length);
Selim Cinek67b22602014-03-10 15:40:16 +0100114 }
115
116
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200117 public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
Selim Cinek67b22602014-03-10 15:40:16 +0100118 // The state of the local variables are saved in an algorithmState to easily subdivide it
119 // into multiple phases.
120 StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
121
122 // First we reset the view states to their default values.
123 resultState.resetViewStates();
124
Selim Cinek343e6e22014-04-11 21:23:30 +0200125 algorithmState.itemsInTopStack = 0.0f;
Selim Cinek67b22602014-03-10 15:40:16 +0100126 algorithmState.partialInTop = 0.0f;
127 algorithmState.lastTopStackIndex = 0;
Selim Cinek343e6e22014-04-11 21:23:30 +0200128 algorithmState.scrolledPixelsTop = 0;
Selim Cinek67b22602014-03-10 15:40:16 +0100129 algorithmState.itemsInBottomStack = 0.0f;
Selim Cinek343e6e22014-04-11 21:23:30 +0200130 algorithmState.partialInBottom = 0.0f;
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200131 algorithmState.scrollY = ambientState.getScrollY() + mCollapsedSize;
Selim Cinek343e6e22014-04-11 21:23:30 +0200132
Jorim Jaggid4a57442014-04-10 02:45:55 +0200133 updateVisibleChildren(resultState, algorithmState);
Selim Cinek67b22602014-03-10 15:40:16 +0100134
135 // Phase 1:
136 findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
137
138 // Phase 2:
139 updatePositionsForState(resultState, algorithmState);
140
141 // Phase 3:
142 updateZValuesForState(resultState, algorithmState);
Selim Cinekeb973562014-05-02 17:07:49 +0200143
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200144 handleDraggedViews(ambientState, resultState, algorithmState);
145 updateDimmedActivated(ambientState, resultState, algorithmState);
146 }
147
148 /**
149 * Updates the dimmed and activated states of the children.
150 */
151 private void updateDimmedActivated(AmbientState ambientState, StackScrollState resultState,
152 StackScrollAlgorithmState algorithmState) {
153 boolean dimmed = ambientState.isDimmed();
154 View activatedChild = ambientState.getActivatedChild();
155 int childCount = algorithmState.visibleChildren.size();
156 for (int i = 0; i < childCount; i++) {
157 View child = algorithmState.visibleChildren.get(i);
158 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
159 childViewState.dimmed = dimmed;
160 childViewState.scale = !dimmed || activatedChild == child
161 ? 1.0f
162 : DIMMED_SCALE;
163 if (dimmed && activatedChild != null && child != activatedChild) {
164 childViewState.alpha *= ACTIVATED_INVERSE_ALPHA;
165 }
166 }
Selim Cinekeb973562014-05-02 17:07:49 +0200167 }
168
169 /**
170 * Handle the special state when views are being dragged
171 */
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200172 private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
Selim Cinekeb973562014-05-02 17:07:49 +0200173 StackScrollAlgorithmState algorithmState) {
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200174 ArrayList<View> draggedViews = ambientState.getDraggedViews();
175 for (View draggedView : draggedViews) {
Selim Cinekeb973562014-05-02 17:07:49 +0200176 int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
177 if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
178 View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200179 if (!draggedViews.contains(nextChild)) {
Selim Cinekeb973562014-05-02 17:07:49 +0200180 // only if the view is not dragged itself we modify its state to be fully
181 // visible
182 StackScrollState.ViewState viewState = resultState.getViewStateForView(
183 nextChild);
184 // The child below the dragged one must be fully visible
185 viewState.alpha = 1;
186 }
187
188 // Lets set the alpha to the one it currently has, as its currently being dragged
189 StackScrollState.ViewState viewState = resultState.getViewStateForView(draggedView);
190 // The dragged child should keep the set alpha
191 viewState.alpha = draggedView.getAlpha();
192 }
193 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200194 }
Selim Cinek67b22602014-03-10 15:40:16 +0100195
Selim Cinek343e6e22014-04-11 21:23:30 +0200196 /**
Jorim Jaggid4a57442014-04-10 02:45:55 +0200197 * Update the visible children on the state.
198 */
199 private void updateVisibleChildren(StackScrollState resultState,
200 StackScrollAlgorithmState state) {
201 ViewGroup hostView = resultState.getHostView();
202 int childCount = hostView.getChildCount();
203 state.visibleChildren.clear();
204 state.visibleChildren.ensureCapacity(childCount);
205 for (int i = 0; i < childCount; i++) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200206 ExpandableView v = (ExpandableView) hostView.getChildAt(i);
Jorim Jaggid4a57442014-04-10 02:45:55 +0200207 if (v.getVisibility() != View.GONE) {
208 state.visibleChildren.add(v);
209 }
210 }
211 }
212
213 /**
Selim Cinek67b22602014-03-10 15:40:16 +0100214 * Determine the positions for the views. This is the main part of the algorithm.
215 *
216 * @param resultState The result state to update if a change to the properties of a child occurs
217 * @param algorithmState The state in which the current pass of the algorithm is currently in
218 * and which will be updated
219 */
220 private void updatePositionsForState(StackScrollState resultState,
221 StackScrollAlgorithmState algorithmState) {
Selim Cinek67b22602014-03-10 15:40:16 +0100222
Selim Cinek1685e632014-04-08 02:27:49 +0200223 // The starting position of the bottom stack peek
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200224 float bottomPeekStart = mInnerHeight - mBottomStackPeekSize;
Selim Cinek1685e632014-04-08 02:27:49 +0200225
Selim Cinek67b22602014-03-10 15:40:16 +0100226 // The position where the bottom stack starts.
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200227 float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
Selim Cinek67b22602014-03-10 15:40:16 +0100228
229 // The y coordinate of the current child.
230 float currentYPosition = 0.0f;
231
232 // How far in is the element currently transitioning into the bottom stack.
233 float yPositionInScrollView = 0.0f;
234
Jorim Jaggid4a57442014-04-10 02:45:55 +0200235 int childCount = algorithmState.visibleChildren.size();
Selim Cinek67b22602014-03-10 15:40:16 +0100236 int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
237 for (int i = 0; i < childCount; i++) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200238 ExpandableView child = algorithmState.visibleChildren.get(i);
Selim Cinek67b22602014-03-10 15:40:16 +0100239 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200240 childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN;
Jorim Jaggibe565df2014-04-28 17:51:23 +0200241 int childHeight = getMaxAllowedChildHeight(child);
Selim Cinek67b22602014-03-10 15:40:16 +0100242 float yPositionInScrollViewAfterElement = yPositionInScrollView
243 + childHeight
244 + mPaddingBetweenElements;
Selim Cinek343e6e22014-04-11 21:23:30 +0200245 float scrollOffset = yPositionInScrollView - algorithmState.scrollY + mCollapsedSize;
246
247 if (i == algorithmState.lastTopStackIndex + 1) {
248 // Normally the position of this child is the position in the regular scrollview,
249 // but if the two stacks are very close to each other,
250 // then have have to push it even more upwards to the position of the bottom
251 // stack start.
252 currentYPosition = Math.min(scrollOffset, bottomStackStart);
253 }
254 childViewState.yTranslation = currentYPosition;
255
256 // The y position after this element
257 float nextYPosition = currentYPosition + childHeight +
258 mPaddingBetweenElements;
259
260 if (i <= algorithmState.lastTopStackIndex) {
Selim Cinek67b22602014-03-10 15:40:16 +0100261 // Case 1:
262 // We are in the top Stack
Selim Cinek343e6e22014-04-11 21:23:30 +0200263 updateStateForTopStackChild(algorithmState,
264 numberOfElementsCompletelyIn, i, childHeight, childViewState, scrollOffset);
265 clampYTranslation(childViewState, childHeight);
266 // check if we are overlapping with the bottom stack
267 if (childViewState.yTranslation + childHeight + mPaddingBetweenElements
Selim Cinek4a1ac842014-05-01 15:51:58 +0200268 >= bottomStackStart && !mIsExpansionChanging && i != 0) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200269 // TODO: handle overlapping sizes with end stack better
270 // we just collapse this element
271 childViewState.height = mCollapsedSize;
272 }
273 } else if (nextYPosition >= bottomStackStart) {
Selim Cinek67b22602014-03-10 15:40:16 +0100274 // Case 2:
Selim Cinek343e6e22014-04-11 21:23:30 +0200275 // We are in the bottom stack.
276 if (currentYPosition >= bottomStackStart) {
Selim Cinek67b22602014-03-10 15:40:16 +0100277 // According to the regular scroll view we are fully translated out of the
278 // bottom of the screen so we are fully in the bottom stack
Selim Cinek343e6e22014-04-11 21:23:30 +0200279 updateStateForChildFullyInBottomStack(algorithmState,
280 bottomStackStart, childViewState, childHeight);
Selim Cinek67b22602014-03-10 15:40:16 +0100281 } else {
Selim Cinek67b22602014-03-10 15:40:16 +0100282 // According to the regular scroll view we are currently translating out of /
283 // into the bottom of the screen
Selim Cinek343e6e22014-04-11 21:23:30 +0200284 updateStateForChildTransitioningInBottom(algorithmState,
285 bottomStackStart, bottomPeekStart, currentYPosition,
286 childViewState, childHeight);
Selim Cinek67b22602014-03-10 15:40:16 +0100287 }
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200288 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200289 // Case 3:
290 // We are in the regular scroll area.
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200291 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
Selim Cinek343e6e22014-04-11 21:23:30 +0200292 clampYTranslation(childViewState, childHeight);
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200293 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200294
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200295 // The first card is always rendered.
296 if (i == 0) {
297 childViewState.alpha = 1.0f;
Selim Cinek343e6e22014-04-11 21:23:30 +0200298 childViewState.yTranslation = 0;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200299 childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD;
300 }
301 if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) {
302 Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
Selim Cinek67b22602014-03-10 15:40:16 +0100303 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200304 currentYPosition = childViewState.yTranslation + childHeight + mPaddingBetweenElements;
Selim Cinek67b22602014-03-10 15:40:16 +0100305 yPositionInScrollView = yPositionInScrollViewAfterElement;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200306
307 childViewState.yTranslation += mTopPadding;
Selim Cinek67b22602014-03-10 15:40:16 +0100308 }
309 }
310
Selim Cinek1685e632014-04-08 02:27:49 +0200311 /**
Selim Cinek343e6e22014-04-11 21:23:30 +0200312 * Clamp the yTranslation both up and down to valid positions.
Selim Cinek1685e632014-04-08 02:27:49 +0200313 *
Selim Cinek1685e632014-04-08 02:27:49 +0200314 * @param childViewState the view state of the child
Selim Cinek343e6e22014-04-11 21:23:30 +0200315 * @param childHeight the height of this child
Selim Cinek1685e632014-04-08 02:27:49 +0200316 */
Selim Cinek343e6e22014-04-11 21:23:30 +0200317 private void clampYTranslation(StackScrollState.ViewState childViewState, int childHeight) {
318 clampPositionToBottomStackStart(childViewState, childHeight);
319 clampPositionToTopStackEnd(childViewState, childHeight);
320 }
321
322 /**
323 * Clamp the yTranslation of the child down such that its end is at most on the beginning of
324 * the bottom stack.
325 *
326 * @param childViewState the view state of the child
327 * @param childHeight the height of this child
328 */
329 private void clampPositionToBottomStackStart(StackScrollState.ViewState childViewState,
330 int childHeight) {
331 childViewState.yTranslation = Math.min(childViewState.yTranslation,
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200332 mInnerHeight - mBottomStackPeekSize - childHeight);
Selim Cinek343e6e22014-04-11 21:23:30 +0200333 }
334
335 /**
336 * Clamp the yTranslation of the child up such that its end is at lest on the end of the top
Jorim Jaggibe565df2014-04-28 17:51:23 +0200337 * stack.get
Selim Cinek343e6e22014-04-11 21:23:30 +0200338 *
339 * @param childViewState the view state of the child
340 * @param childHeight the height of this child
341 */
342 private void clampPositionToTopStackEnd(StackScrollState.ViewState childViewState,
343 int childHeight) {
344 childViewState.yTranslation = Math.max(childViewState.yTranslation,
345 mCollapsedSize - childHeight);
Selim Cinek1685e632014-04-08 02:27:49 +0200346 }
347
348 private int getMaxAllowedChildHeight(View child) {
349 if (child instanceof ExpandableNotificationRow) {
350 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
Jorim Jaggi9cbadd32014-05-01 20:18:31 +0200351 return row.getIntrinsicHeight();
Jorim Jaggibe565df2014-04-28 17:51:23 +0200352 } else if (child instanceof ExpandableView) {
353 ExpandableView expandableView = (ExpandableView) child;
354 return expandableView.getActualHeight();
Selim Cinek1685e632014-04-08 02:27:49 +0200355 }
Jorim Jaggibe565df2014-04-28 17:51:23 +0200356 return child == null? mCollapsedSize : child.getHeight();
Selim Cinek1685e632014-04-08 02:27:49 +0200357 }
358
Selim Cinek343e6e22014-04-11 21:23:30 +0200359 private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
360 float transitioningPositionStart, float bottomPeakStart, float currentYPosition,
361 StackScrollState.ViewState childViewState, int childHeight) {
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200362
Selim Cinek343e6e22014-04-11 21:23:30 +0200363 // This is the transitioning element on top of bottom stack, calculate how far we are in.
364 algorithmState.partialInBottom = 1.0f - (
365 (transitioningPositionStart - currentYPosition) / (childHeight +
366 mPaddingBetweenElements));
367
368 // the offset starting at the transitionPosition of the bottom stack
369 float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom);
370 algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200371 childViewState.yTranslation = transitioningPositionStart + offset - childHeight
372 - mPaddingBetweenElements;
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200373
Selim Cinek343e6e22014-04-11 21:23:30 +0200374 // We want at least to be at the end of the top stack when collapsing
375 clampPositionToTopStackEnd(childViewState, childHeight);
376 childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
Selim Cinek67b22602014-03-10 15:40:16 +0100377 }
378
Selim Cinek343e6e22014-04-11 21:23:30 +0200379 private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
Selim Cinek67b22602014-03-10 15:40:16 +0100380 float transitioningPositionStart, StackScrollState.ViewState childViewState,
381 int childHeight) {
382
Selim Cinek343e6e22014-04-11 21:23:30 +0200383 float currentYPosition;
Selim Cinek67b22602014-03-10 15:40:16 +0100384 algorithmState.itemsInBottomStack += 1.0f;
385 if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
386 // We are visually entering the bottom stack
Selim Cinek343e6e22014-04-11 21:23:30 +0200387 currentYPosition = transitioningPositionStart
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200388 + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack)
389 - mPaddingBetweenElements;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200390 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING;
Selim Cinek67b22602014-03-10 15:40:16 +0100391 } else {
392 // we are fully inside the stack
393 if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
394 childViewState.alpha = 0.0f;
395 } else if (algorithmState.itemsInBottomStack
396 > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
397 childViewState.alpha = 1.0f - algorithmState.partialInBottom;
398 }
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200399 childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200400 currentYPosition = mInnerHeight;
Selim Cinek67b22602014-03-10 15:40:16 +0100401 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200402 childViewState.yTranslation = currentYPosition - childHeight;
403 clampPositionToTopStackEnd(childViewState, childHeight);
Selim Cinek67b22602014-03-10 15:40:16 +0100404 }
405
Selim Cinek343e6e22014-04-11 21:23:30 +0200406 private void updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
407 int numberOfElementsCompletelyIn, int i, int childHeight,
408 StackScrollState.ViewState childViewState, float scrollOffset) {
Selim Cinek67b22602014-03-10 15:40:16 +0100409
Selim Cinek67b22602014-03-10 15:40:16 +0100410
411 // First we calculate the index relative to the current stack window of size at most
412 // {@link #MAX_ITEMS_IN_TOP_STACK}
Selim Cinek343e6e22014-04-11 21:23:30 +0200413 int paddedIndex = i - 1
Selim Cinek67b22602014-03-10 15:40:16 +0100414 - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
415 if (paddedIndex >= 0) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200416
Selim Cinek67b22602014-03-10 15:40:16 +0100417 // We are currently visually entering the top stack
Selim Cinek343e6e22014-04-11 21:23:30 +0200418 float distanceToStack = childHeight - algorithmState.scrolledPixelsTop;
419 if (i == algorithmState.lastTopStackIndex && distanceToStack > mTopStackTotalSize) {
420
421 // Child is currently translating into stack but not yet inside slow down zone.
422 // Handle it like the regular scrollview.
423 childViewState.yTranslation = scrollOffset;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200424 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200425 // Apply stacking logic.
426 float numItemsBefore;
427 if (i == algorithmState.lastTopStackIndex) {
428 numItemsBefore = 1.0f - (distanceToStack / mTopStackTotalSize);
429 } else {
430 numItemsBefore = algorithmState.itemsInTopStack - i;
431 }
432 // The end position of the current child
433 float currentChildEndY = mCollapsedSize + mTopStackTotalSize -
434 mTopStackIndentationFunctor.getValue(numItemsBefore);
435 childViewState.yTranslation = currentChildEndY - childHeight;
Selim Cinek67b22602014-03-10 15:40:16 +0100436 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200437 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
Selim Cinek67b22602014-03-10 15:40:16 +0100438 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200439 if (paddedIndex == -1) {
440 childViewState.alpha = 1.0f - algorithmState.partialInTop;
441 } else {
442 // We are hidden behind the top card and faded out, so we can hide ourselves.
443 childViewState.alpha = 0.0f;
444 }
445 childViewState.yTranslation = mCollapsedSize - childHeight;
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200446 childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
Selim Cinek67b22602014-03-10 15:40:16 +0100447 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200448
449
Selim Cinek67b22602014-03-10 15:40:16 +0100450 }
451
452 /**
453 * Find the number of items in the top stack and update the result state if needed.
454 *
455 * @param resultState The result state to update if a height change of an child occurs
456 * @param algorithmState The state in which the current pass of the algorithm is currently in
457 * and which will be updated
458 */
459 private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
460 StackScrollAlgorithmState algorithmState) {
461
462 // The y Position if the element would be in a regular scrollView
463 float yPositionInScrollView = 0.0f;
Jorim Jaggid4a57442014-04-10 02:45:55 +0200464 int childCount = algorithmState.visibleChildren.size();
Selim Cinek67b22602014-03-10 15:40:16 +0100465
466 // find the number of elements in the top stack.
467 for (int i = 0; i < childCount; i++) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200468 ExpandableView child = algorithmState.visibleChildren.get(i);
Selim Cinek67b22602014-03-10 15:40:16 +0100469 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
Jorim Jaggibe565df2014-04-28 17:51:23 +0200470 int childHeight = getMaxAllowedChildHeight(child);
Selim Cinek67b22602014-03-10 15:40:16 +0100471 float yPositionInScrollViewAfterElement = yPositionInScrollView
472 + childHeight
473 + mPaddingBetweenElements;
474 if (yPositionInScrollView < algorithmState.scrollY) {
Selim Cinek343e6e22014-04-11 21:23:30 +0200475 if (i == 0 && algorithmState.scrollY == mCollapsedSize) {
476
477 // The starting position of the bottom stack peek
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200478 int bottomPeekStart = mInnerHeight - mBottomStackPeekSize;
Selim Cinek343e6e22014-04-11 21:23:30 +0200479 // Collapse and expand the first child while the shade is being expanded
480 float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
481 ? mFirstChildMaxHeight
482 : childHeight;
483 childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight),
484 mCollapsedSize);
485 algorithmState.itemsInTopStack = 1.0f;
486
487 } else if (yPositionInScrollViewAfterElement < algorithmState.scrollY) {
Selim Cinek67b22602014-03-10 15:40:16 +0100488 // According to the regular scroll view we are fully off screen
489 algorithmState.itemsInTopStack += 1.0f;
Selim Cinek343e6e22014-04-11 21:23:30 +0200490 if (i == 0) {
Selim Cinek67b22602014-03-10 15:40:16 +0100491 childViewState.height = mCollapsedSize;
492 }
493 } else {
494 // According to the regular scroll view we are partially off screen
495 // If it is expanded we have to collapse it to a new size
496 float newSize = yPositionInScrollViewAfterElement
497 - mPaddingBetweenElements
498 - algorithmState.scrollY;
499
Selim Cinek343e6e22014-04-11 21:23:30 +0200500 if (i == 0) {
501 newSize += mCollapsedSize;
502 }
503
Selim Cinek67b22602014-03-10 15:40:16 +0100504 // How much did we scroll into this child
Selim Cinek343e6e22014-04-11 21:23:30 +0200505 algorithmState.scrolledPixelsTop = childHeight - newSize;
506 algorithmState.partialInTop = (algorithmState.scrolledPixelsTop) / (childHeight
Selim Cinek67b22602014-03-10 15:40:16 +0100507 + mPaddingBetweenElements);
508
509 // Our element can be expanded, so this can get negative
510 algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
511 algorithmState.itemsInTopStack += algorithmState.partialInTop;
Selim Cinek67b22602014-03-10 15:40:16 +0100512 newSize = Math.max(mCollapsedSize, newSize);
Selim Cinek343e6e22014-04-11 21:23:30 +0200513 if (i == 0) {
Selim Cinek67b22602014-03-10 15:40:16 +0100514 childViewState.height = (int) newSize;
Selim Cinek67b22602014-03-10 15:40:16 +0100515 }
Selim Cinek343e6e22014-04-11 21:23:30 +0200516 algorithmState.lastTopStackIndex = i;
517 break;
Selim Cinek67b22602014-03-10 15:40:16 +0100518 }
519 } else {
Selim Cinek343e6e22014-04-11 21:23:30 +0200520 algorithmState.lastTopStackIndex = i - 1;
Selim Cinek67b22602014-03-10 15:40:16 +0100521 // We are already past the stack so we can end the loop
522 break;
523 }
524 yPositionInScrollView = yPositionInScrollViewAfterElement;
525 }
526 }
527
528 /**
529 * Calculate the Z positions for all children based on the number of items in both stacks and
530 * save it in the resultState
531 *
532 * @param resultState The result state to update the zTranslation values
533 * @param algorithmState The state in which the current pass of the algorithm is currently in
534 */
535 private void updateZValuesForState(StackScrollState resultState,
536 StackScrollAlgorithmState algorithmState) {
Jorim Jaggid4a57442014-04-10 02:45:55 +0200537 int childCount = algorithmState.visibleChildren.size();
Selim Cinek67b22602014-03-10 15:40:16 +0100538 for (int i = 0; i < childCount; i++) {
Jorim Jaggid4a57442014-04-10 02:45:55 +0200539 View child = algorithmState.visibleChildren.get(i);
Selim Cinek67b22602014-03-10 15:40:16 +0100540 StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
541 if (i < algorithmState.itemsInTopStack) {
542 float stackIndex = algorithmState.itemsInTopStack - i;
543 stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
544 childViewState.zTranslation = mZBasicHeight
545 + stackIndex * mZDistanceBetweenElements;
546 } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
547 float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
548 float translationZ = mZBasicHeight
549 - numItemsAbove * mZDistanceBetweenElements;
550 childViewState.zTranslation = translationZ;
551 } else {
552 childViewState.zTranslation = mZBasicHeight;
553 }
554 }
555 }
556
Selim Cinek343e6e22014-04-11 21:23:30 +0200557 public void setLayoutHeight(int layoutHeight) {
Selim Cinek67b22602014-03-10 15:40:16 +0100558 this.mLayoutHeight = layoutHeight;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200559 updateInnerHeight();
560 }
561
562 public void setTopPadding(int topPadding) {
563 mTopPadding = topPadding;
564 updateInnerHeight();
565 }
566
567 private void updateInnerHeight() {
568 mInnerHeight = mLayoutHeight - mTopPadding;
Selim Cinek67b22602014-03-10 15:40:16 +0100569 }
570
Selim Cinek1685e632014-04-08 02:27:49 +0200571 public void onExpansionStarted(StackScrollState currentState) {
572 mIsExpansionChanging = true;
573 mExpandedOnStart = mIsExpanded;
574 ViewGroup hostView = currentState.getHostView();
575 updateFirstChildHeightWhileExpanding(hostView);
576 }
577
578 private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200579 mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView);
Jorim Jaggi9f347ae2014-04-11 00:47:18 +0200580 if (mFirstChildWhileExpanding != null) {
Selim Cinek1685e632014-04-08 02:27:49 +0200581 if (mExpandedOnStart) {
582
583 // We are collapsing the shade, so the first child can get as most as high as the
584 // current height.
Jorim Jaggibe565df2014-04-28 17:51:23 +0200585 mFirstChildMaxHeight = mFirstChildWhileExpanding.getActualHeight();
Selim Cinek1685e632014-04-08 02:27:49 +0200586 } else {
587
588 // We are expanding the shade, expand it to its full height.
Selim Cinekb6e0e122014-04-23 17:24:37 +0200589 if (mFirstChildWhileExpanding.getWidth() == 0) {
590
591 // This child was not layouted yet, wait for a layout pass
592 mFirstChildWhileExpanding
593 .addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
594 @Override
595 public void onLayoutChange(View v, int left, int top, int right,
596 int bottom, int oldLeft, int oldTop, int oldRight,
597 int oldBottom) {
Selim Cinek2ba5f1f2014-04-28 20:23:30 +0200598 if (mFirstChildWhileExpanding != null) {
599 mFirstChildMaxHeight = getMaxAllowedChildHeight(
600 mFirstChildWhileExpanding);
601 } else {
602 mFirstChildMaxHeight = 0;
603 }
604 v.removeOnLayoutChangeListener(this);
Selim Cinekb6e0e122014-04-23 17:24:37 +0200605 }
606 });
607 } else {
608 mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding);
609 }
Selim Cinek1685e632014-04-08 02:27:49 +0200610 }
611 } else {
Selim Cinek1685e632014-04-08 02:27:49 +0200612 mFirstChildMaxHeight = 0;
613 }
614 }
615
Jorim Jaggi9f347ae2014-04-11 00:47:18 +0200616 private View findFirstVisibleChild(ViewGroup container) {
617 int childCount = container.getChildCount();
618 for (int i = 0; i < childCount; i++) {
619 View child = container.getChildAt(i);
620 if (child.getVisibility() != View.GONE) {
621 return child;
622 }
623 }
624 return null;
625 }
626
Selim Cinek1685e632014-04-08 02:27:49 +0200627 public void onExpansionStopped() {
628 mIsExpansionChanging = false;
629 mFirstChildWhileExpanding = null;
630 }
631
632 public void setIsExpanded(boolean isExpanded) {
633 this.mIsExpanded = isExpanded;
634 }
635
636 public void notifyChildrenChanged(ViewGroup hostView) {
637 if (mIsExpansionChanging) {
638 updateFirstChildHeightWhileExpanding(hostView);
639 }
640 }
641
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200642 public void setDimmed(boolean dimmed) {
643 updatePadding(dimmed);
644 }
645
Selim Cinek67b22602014-03-10 15:40:16 +0100646 class StackScrollAlgorithmState {
647
648 /**
649 * The scroll position of the algorithm
650 */
651 public int scrollY;
652
653 /**
654 * The quantity of items which are in the top stack.
655 */
656 public float itemsInTopStack;
657
658 /**
659 * how far in is the element currently transitioning into the top stack
660 */
661 public float partialInTop;
662
663 /**
Selim Cinek343e6e22014-04-11 21:23:30 +0200664 * The number of pixels the last child in the top stack has scrolled in to the stack
665 */
666 public float scrolledPixelsTop;
667
668 /**
Selim Cinek67b22602014-03-10 15:40:16 +0100669 * The last item index which is in the top stack.
Selim Cinek67b22602014-03-10 15:40:16 +0100670 */
671 public int lastTopStackIndex;
672
673 /**
674 * The quantity of items which are in the bottom stack.
675 */
676 public float itemsInBottomStack;
677
678 /**
679 * how far in is the element currently transitioning into the bottom stack
680 */
681 public float partialInBottom;
Jorim Jaggid4a57442014-04-10 02:45:55 +0200682
683 /**
684 * The children from the host view which are not gone.
685 */
Jorim Jaggibe565df2014-04-28 17:51:23 +0200686 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
Selim Cinek67b22602014-03-10 15:40:16 +0100687 }
688
689}