blob: b46d523a499abf02795bcb05459ca8489ad0dc44 [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;
20import android.content.res.Configuration;
Selim Cinek67b22602014-03-10 15:40:16 +010021import android.graphics.Canvas;
Selim Cinek67b22602014-03-10 15:40:16 +010022import android.graphics.Paint;
23import android.util.AttributeSet;
24import android.util.Log;
Selim Cinek67b22602014-03-10 15:40:16 +010025import android.view.MotionEvent;
26import android.view.VelocityTracker;
27import android.view.View;
28import android.view.ViewConfiguration;
29import android.view.ViewGroup;
Selim Cinek343e6e22014-04-11 21:23:30 +020030import android.view.ViewTreeObserver;
Selim Cinek572bbd42014-04-25 16:43:27 +020031import android.view.animation.AnimationUtils;
Selim Cinek67b22602014-03-10 15:40:16 +010032import android.widget.OverScroller;
Selim Cinek67b22602014-03-10 15:40:16 +010033import com.android.systemui.ExpandHelper;
34import com.android.systemui.R;
35import com.android.systemui.SwipeHelper;
Selim Cineka32ab602014-06-11 15:06:01 +020036import com.android.systemui.statusbar.ActivatableNotificationView;
Selim Cinek67b22602014-03-10 15:40:16 +010037import com.android.systemui.statusbar.ExpandableNotificationRow;
Jorim Jaggibe565df2014-04-28 17:51:23 +020038import com.android.systemui.statusbar.ExpandableView;
Selim Cinekc27437b2014-05-14 10:23:33 +020039import com.android.systemui.statusbar.SpeedBumpView;
Selim Cinekb6d85eb2014-03-28 20:21:01 +010040import com.android.systemui.statusbar.policy.ScrollAdapter;
Jorim Jaggi290600a2014-05-30 17:02:20 +020041import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
Selim Cinek67b22602014-03-10 15:40:16 +010042
Selim Cinek572bbd42014-04-25 16:43:27 +020043import java.util.ArrayList;
44
Selim Cinek67b22602014-03-10 15:40:16 +010045/**
46 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
47 */
48public class NotificationStackScrollLayout extends ViewGroup
Jorim Jaggibe565df2014-04-28 17:51:23 +020049 implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
50 ExpandableView.OnHeightChangedListener {
Selim Cinek67b22602014-03-10 15:40:16 +010051
52 private static final String TAG = "NotificationStackScrollLayout";
53 private static final boolean DEBUG = false;
Selim Cinek1408eb52014-06-02 14:45:38 +020054 private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
55 private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
Jorim Jaggi47c85a32014-06-05 17:25:40 +020056 private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
Selim Cinek67b22602014-03-10 15:40:16 +010057
58 /**
59 * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
60 */
61 private static final int INVALID_POINTER = -1;
62
Selim Cinek1408eb52014-06-02 14:45:38 +020063 private ExpandHelper mExpandHelper;
Selim Cinek67b22602014-03-10 15:40:16 +010064 private SwipeHelper mSwipeHelper;
Selim Cinek343e6e22014-04-11 21:23:30 +020065 private boolean mSwipingInProgress;
Selim Cinek67b22602014-03-10 15:40:16 +010066 private int mCurrentStackHeight = Integer.MAX_VALUE;
67 private int mOwnScrollY;
68 private int mMaxLayoutHeight;
69
70 private VelocityTracker mVelocityTracker;
71 private OverScroller mScroller;
72 private int mTouchSlop;
73 private int mMinimumVelocity;
74 private int mMaximumVelocity;
Selim Cinek67b22602014-03-10 15:40:16 +010075 private int mOverflingDistance;
Selim Cinek8d9ff9c2014-05-12 15:13:04 +020076 private float mMaxOverScroll;
Selim Cinek67b22602014-03-10 15:40:16 +010077 private boolean mIsBeingDragged;
78 private int mLastMotionY;
Selim Cinek1408eb52014-06-02 14:45:38 +020079 private int mDownX;
Selim Cinek67b22602014-03-10 15:40:16 +010080 private int mActivePointerId;
81
82 private int mSidePaddings;
83 private Paint mDebugPaint;
Selim Cinek67b22602014-03-10 15:40:16 +010084 private int mContentHeight;
85 private int mCollapsedSize;
Selim Cineka5eaa602014-05-12 21:27:47 +020086 private int mBottomStackSlowDownHeight;
Selim Cinek67b22602014-03-10 15:40:16 +010087 private int mBottomStackPeekSize;
Selim Cinek67b22602014-03-10 15:40:16 +010088 private int mPaddingBetweenElements;
Selim Cineka5eaa602014-05-12 21:27:47 +020089 private int mPaddingBetweenElementsDimmed;
90 private int mPaddingBetweenElementsNormal;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +020091 private int mTopPadding;
Selim Cinek67b22602014-03-10 15:40:16 +010092
93 /**
94 * The algorithm which calculates the properties for our children
95 */
96 private StackScrollAlgorithm mStackScrollAlgorithm;
97
98 /**
99 * The current State this Layout is in
100 */
Selim Cinek572bbd42014-04-25 16:43:27 +0200101 private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200102 private AmbientState mAmbientState = new AmbientState();
Selim Cinek572bbd42014-04-25 16:43:27 +0200103 private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
104 private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
Selim Cinekeb973562014-05-02 17:07:49 +0200105 private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
106 private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>();
Selim Cinek8efa6dd2014-05-19 16:27:37 +0200107 private ArrayList<View> mChildrenChangingPositions = new ArrayList<View>();
Jorim Jaggi0dd68812014-05-01 19:17:37 +0200108 private ArrayList<AnimationEvent> mAnimationEvents
109 = new ArrayList<AnimationEvent>();
Selim Cinek572bbd42014-04-25 16:43:27 +0200110 private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
111 private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
Jorim Jaggi75c95042014-05-16 19:09:59 +0200112 private boolean mAnimationsEnabled;
Selim Cinek159ffdb2014-06-04 22:24:18 +0200113 private boolean mChangePositionInProgress;
Selim Cinek1685e632014-04-08 02:27:49 +0200114
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200115 /**
116 * The raw amount of the overScroll on the top, which is not rubber-banded.
117 */
118 private float mOverScrolledTopPixels;
119
120 /**
121 * The raw amount of the overScroll on the bottom, which is not rubber-banded.
122 */
123 private float mOverScrolledBottomPixels;
124
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200125 private OnChildLocationsChangedListener mListener;
Jorim Jaggi290600a2014-05-30 17:02:20 +0200126 private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
Jorim Jaggibe565df2014-04-28 17:51:23 +0200127 private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
Jorim Jaggi0dd68812014-05-01 19:17:37 +0200128 private boolean mNeedsAnimation;
129 private boolean mTopPaddingNeedsAnimation;
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200130 private boolean mDimmedNeedsAnimation;
John Spurlockbf370992014-06-17 13:58:31 -0400131 private boolean mDarkNeedsAnimation;
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200132 private boolean mActivateNeedsAnimation;
Selim Cinek572bbd42014-04-25 16:43:27 +0200133 private boolean mIsExpanded = true;
Selim Cinek1f553cf2014-05-02 12:01:36 +0200134 private boolean mChildrenUpdateRequested;
Selim Cinekc27437b2014-05-14 10:23:33 +0200135 private SpeedBumpView mSpeedBumpView;
136 private boolean mIsExpansionChanging;
Selim Cinek1408eb52014-06-02 14:45:38 +0200137 private boolean mExpandingNotification;
138 private boolean mExpandedInThisMotion;
139 private boolean mScrollingEnabled;
140
141 /**
142 * Was the scroller scrolled to the top when the down motion was observed?
143 */
144 private boolean mScrolledToTopOnFirstDown;
145
146 /**
147 * The minimal amount of over scroll which is needed in order to switch to the quick settings
148 * when over scrolling on a expanded card.
149 */
150 private float mMinTopOverScrollToEscape;
151 private int mIntrinsicPadding;
152 private int mNotificationTopPadding;
Jorim Jaggi30c305c2014-07-01 23:34:41 +0200153 private float mTopPaddingOverflow;
Selim Cinek1408eb52014-06-02 14:45:38 +0200154 private boolean mDontReportNextOverScroll;
155
156 /**
157 * The maximum scrollPosition which we are allowed to reach when a notification was expanded.
158 * This is needed to avoid scrolling too far after the notification was collapsed in the same
159 * motion.
160 */
161 private int mMaxScrollAfterExpand;
Selim Cinek56109b02014-06-04 16:47:54 +0200162 private OnLongClickListener mLongClickListener;
Selim Cinek1408eb52014-06-02 14:45:38 +0200163
164 /**
165 * Should in this touch motion only be scrolling allowed? It's true when the scroller was
166 * animating.
167 */
168 private boolean mOnlyScrollingInThisMotion;
Selim Cinek5158d822014-06-04 13:20:41 +0200169 private boolean mTouchEnabled = true;
Selim Cinek1f553cf2014-05-02 12:01:36 +0200170 private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
Selim Cinek572bbd42014-04-25 16:43:27 +0200171 = new ViewTreeObserver.OnPreDrawListener() {
172 @Override
173 public boolean onPreDraw() {
Selim Cinek1f553cf2014-05-02 12:01:36 +0200174 updateChildren();
175 mChildrenUpdateRequested = false;
176 getViewTreeObserver().removeOnPreDrawListener(this);
Selim Cinek572bbd42014-04-25 16:43:27 +0200177 return true;
178 }
179 };
Selim Cinek67b22602014-03-10 15:40:16 +0100180
181 public NotificationStackScrollLayout(Context context) {
182 this(context, null);
183 }
184
185 public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
186 this(context, attrs, 0);
187 }
188
189 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
190 this(context, attrs, defStyleAttr, 0);
191 }
192
193 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
194 int defStyleRes) {
195 super(context, attrs, defStyleAttr, defStyleRes);
196 initView(context);
197 if (DEBUG) {
198 setWillNotDraw(false);
199 mDebugPaint = new Paint();
200 mDebugPaint.setColor(0xffff0000);
201 mDebugPaint.setStrokeWidth(2);
202 mDebugPaint.setStyle(Paint.Style.STROKE);
203 }
204 }
205
206 @Override
207 protected void onDraw(Canvas canvas) {
208 if (DEBUG) {
209 int y = mCollapsedSize;
210 canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200211 y = (int) (getLayoutHeight() - mBottomStackPeekSize
Selim Cineka5eaa602014-05-12 21:27:47 +0200212 - mBottomStackSlowDownHeight);
Selim Cinek34c0a8d2014-05-12 00:01:43 +0200213 canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
214 y = (int) (getLayoutHeight() - mBottomStackPeekSize);
Selim Cinek67b22602014-03-10 15:40:16 +0100215 canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
216 y = (int) getLayoutHeight();
217 canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
Jorim Jaggi1d480692014-05-20 19:41:58 +0200218 y = getHeight() - getEmptyBottomMargin();
219 canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
Selim Cinek67b22602014-03-10 15:40:16 +0100220 }
221 }
222
223 private void initView(Context context) {
224 mScroller = new OverScroller(getContext());
225 setFocusable(true);
226 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
Jorim Jaggi4222d9a2014-04-23 16:13:15 +0200227 setClipChildren(false);
Selim Cinek67b22602014-03-10 15:40:16 +0100228 final ViewConfiguration configuration = ViewConfiguration.get(context);
229 mTouchSlop = configuration.getScaledTouchSlop();
230 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
231 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Selim Cinek67b22602014-03-10 15:40:16 +0100232 mOverflingDistance = configuration.getScaledOverflingDistance();
233 float densityScale = getResources().getDisplayMetrics().density;
234 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
235 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
Selim Cinek56109b02014-06-04 16:47:54 +0200236 mSwipeHelper.setLongPressListener(mLongClickListener);
Selim Cinek67b22602014-03-10 15:40:16 +0100237
238 mSidePaddings = context.getResources()
239 .getDimensionPixelSize(R.dimen.notification_side_padding);
Selim Cinek67b22602014-03-10 15:40:16 +0100240 mCollapsedSize = context.getResources()
Jorim Jaggife40f7d2014-04-28 15:20:04 +0200241 .getDimensionPixelSize(R.dimen.notification_min_height);
Selim Cinek67b22602014-03-10 15:40:16 +0100242 mBottomStackPeekSize = context.getResources()
243 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
Selim Cinek67b22602014-03-10 15:40:16 +0100244 mStackScrollAlgorithm = new StackScrollAlgorithm(context);
Jorim Jaggi220bf5c2014-06-14 22:43:12 +0200245 mStackScrollAlgorithm.setDimmed(mAmbientState.isDimmed());
Selim Cineka5eaa602014-05-12 21:27:47 +0200246 mPaddingBetweenElementsDimmed = context.getResources()
247 .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
248 mPaddingBetweenElementsNormal = context.getResources()
249 .getDimensionPixelSize(R.dimen.notification_padding);
Jorim Jaggi220bf5c2014-06-14 22:43:12 +0200250 updatePadding(mAmbientState.isDimmed());
Selim Cinek1408eb52014-06-02 14:45:38 +0200251 int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
252 int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height);
253 mExpandHelper = new ExpandHelper(getContext(), this,
254 minHeight, maxHeight);
255 mExpandHelper.setEventSource(this);
256 mExpandHelper.setScrollAdapter(this);
257 mMinTopOverScrollToEscape = getResources().getDimensionPixelSize(
258 R.dimen.min_top_overscroll_to_qs);
259 mNotificationTopPadding = getResources().getDimensionPixelSize(
260 R.dimen.notifications_top_padding);
Selim Cineka5eaa602014-05-12 21:27:47 +0200261 }
262
263 private void updatePadding(boolean dimmed) {
264 mPaddingBetweenElements = dimmed
265 ? mPaddingBetweenElementsDimmed
266 : mPaddingBetweenElementsNormal;
267 mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength();
268 updateContentHeight();
Selim Cinekaef92ef2014-06-06 18:06:04 +0200269 notifyHeightChangeListener(null);
270 }
271
272 private void notifyHeightChangeListener(ExpandableView view) {
273 if (mOnHeightChangedListener != null) {
274 mOnHeightChangedListener.onHeightChanged(view);
275 }
Selim Cinek67b22602014-03-10 15:40:16 +0100276 }
277
278 @Override
279 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
280 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
281 int mode = MeasureSpec.getMode(widthMeasureSpec);
282 int size = MeasureSpec.getSize(widthMeasureSpec);
283 int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
284 measureChildren(childMeasureSpec, heightMeasureSpec);
285 }
286
287 @Override
288 protected void onLayout(boolean changed, int l, int t, int r, int b) {
289
290 // we layout all our children centered on the top
291 float centerX = getWidth() / 2.0f;
292 for (int i = 0; i < getChildCount(); i++) {
293 View child = getChildAt(i);
294 float width = child.getMeasuredWidth();
295 float height = child.getMeasuredHeight();
Selim Cinek67b22602014-03-10 15:40:16 +0100296 child.layout((int) (centerX - width / 2.0f),
297 0,
298 (int) (centerX + width / 2.0f),
299 (int) height);
Selim Cinek67b22602014-03-10 15:40:16 +0100300 }
Jorim Jaggi1d480692014-05-20 19:41:58 +0200301 setMaxLayoutHeight(getHeight());
Selim Cinek67b22602014-03-10 15:40:16 +0100302 updateContentHeight();
Selim Cinek319bdc42014-05-01 23:01:58 +0200303 updateScrollPositionIfNecessary();
304 requestChildrenUpdate();
Selim Cinek67b22602014-03-10 15:40:16 +0100305 }
306
Selim Cinekc27437b2014-05-14 10:23:33 +0200307 public void updateSpeedBumpIndex(int newIndex) {
308 int currentIndex = indexOfChild(mSpeedBumpView);
309
310 // If we are currently layouted before the new speed bump index, we have to decrease it.
311 boolean validIndex = newIndex > 0;
312 if (newIndex > getChildCount() - 1) {
313 validIndex = false;
314 newIndex = -1;
315 }
316 if (validIndex && currentIndex != newIndex) {
317 changeViewPosition(mSpeedBumpView, newIndex);
318 }
319 updateSpeedBump(validIndex);
320 mAmbientState.setSpeedBumpIndex(newIndex);
321 }
322
Christoph Studer6e3eceb2014-04-01 18:40:27 +0200323 public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
324 mListener = listener;
325 }
326
327 /**
328 * Returns the location the given child is currently rendered at.
329 *
330 * @param child the child to get the location for
331 * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants
332 */
333 public int getChildLocation(View child) {
334 ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
335 if (childViewState == null) {
336 return ViewState.LOCATION_UNKNOWN;
337 }
338 return childViewState.location;
339 }
340
Selim Cinek67b22602014-03-10 15:40:16 +0100341 private void setMaxLayoutHeight(int maxLayoutHeight) {
342 mMaxLayoutHeight = maxLayoutHeight;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200343 updateAlgorithmHeightAndPadding();
Selim Cinek67b22602014-03-10 15:40:16 +0100344 }
345
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200346 private void updateAlgorithmHeightAndPadding() {
Selim Cinek67b22602014-03-10 15:40:16 +0100347 mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200348 mStackScrollAlgorithm.setTopPadding(mTopPadding);
Selim Cinek67b22602014-03-10 15:40:16 +0100349 }
350
351 /**
Selim Cinek4a1ac842014-05-01 15:51:58 +0200352 * @return whether the height of the layout needs to be adapted, in order to ensure that the
353 * last child is not in the bottom stack.
354 */
355 private boolean needsHeightAdaption() {
Jorim Jaggi1d480692014-05-20 19:41:58 +0200356 return getNotGoneChildCount() > 1;
Selim Cinek4a1ac842014-05-01 15:51:58 +0200357 }
358
Selim Cinek4a1ac842014-05-01 15:51:58 +0200359 /**
Selim Cinek67b22602014-03-10 15:40:16 +0100360 * Updates the children views according to the stack scroll algorithm. Call this whenever
361 * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
362 */
363 private void updateChildren() {
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200364 mAmbientState.setScrollY(mOwnScrollY);
365 mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
Jorim Jaggi0dd68812014-05-01 19:17:37 +0200366 if (!isCurrentlyAnimating() && !mNeedsAnimation) {
Selim Cinek572bbd42014-04-25 16:43:27 +0200367 applyCurrentState();
Selim Cinek67b22602014-03-10 15:40:16 +0100368 } else {
Selim Cinekf4c19962014-05-01 21:55:31 +0200369 startAnimationToState();
Selim Cinek67b22602014-03-10 15:40:16 +0100370 }
371 }
372
Selim Cinek319bdc42014-05-01 23:01:58 +0200373 private void requestChildrenUpdate() {
Selim Cinek1f553cf2014-05-02 12:01:36 +0200374 if (!mChildrenUpdateRequested) {
375 getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
376 mChildrenUpdateRequested = true;
377 invalidate();
378 }
Selim Cinek319bdc42014-05-01 23:01:58 +0200379 }
380
Selim Cinek67b22602014-03-10 15:40:16 +0100381 private boolean isCurrentlyAnimating() {
Selim Cinek572bbd42014-04-25 16:43:27 +0200382 return mStateAnimator.isRunning();
Selim Cinek67b22602014-03-10 15:40:16 +0100383 }
384
Selim Cinek67b22602014-03-10 15:40:16 +0100385 private void updateScrollPositionIfNecessary() {
386 int scrollRange = getScrollRange();
387 if (scrollRange < mOwnScrollY) {
388 mOwnScrollY = scrollRange;
389 }
390 }
391
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200392 public int getTopPadding() {
393 return mTopPadding;
394 }
395
Selim Cinek1408eb52014-06-02 14:45:38 +0200396 private void setTopPadding(int topPadding, boolean animate) {
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200397 if (mTopPadding != topPadding) {
398 mTopPadding = topPadding;
399 updateAlgorithmHeightAndPadding();
400 updateContentHeight();
Jorim Jaggi75c95042014-05-16 19:09:59 +0200401 if (animate && mAnimationsEnabled && mIsExpanded) {
Jorim Jaggi0dd68812014-05-01 19:17:37 +0200402 mTopPaddingNeedsAnimation = true;
403 mNeedsAnimation = true;
404 }
Selim Cinek319bdc42014-05-01 23:01:58 +0200405 requestChildrenUpdate();
Selim Cinekaef92ef2014-06-06 18:06:04 +0200406 notifyHeightChangeListener(null);
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200407 }
408 }
409
410 /**
411 * Update the height of the stack to a new height.
412 *
413 * @param height the new height of the stack
414 */
415 public void setStackHeight(float height) {
416 setIsExpanded(height > 0.0f);
417 int newStackHeight = (int) height;
418 int itemHeight = getItemHeight();
419 int bottomStackPeekSize = mBottomStackPeekSize;
420 int minStackHeight = itemHeight + bottomStackPeekSize;
421 int stackHeight;
Jorim Jaggi2580a9762014-06-25 03:08:25 +0200422 if (newStackHeight - mTopPadding >= minStackHeight || getNotGoneChildCount() == 0) {
Jorim Jaggi30c305c2014-07-01 23:34:41 +0200423 setTranslationY(mTopPaddingOverflow);
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200424 stackHeight = newStackHeight;
425 } else {
426
427 // We did not reach the position yet where we actually start growing,
428 // so we translate the stack upwards.
429 int translationY = (newStackHeight - minStackHeight);
430 // A slight parallax effect is introduced in order for the stack to catch up with
431 // the top card.
432 float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight;
433 partiallyThere = Math.max(0, partiallyThere);
434 translationY += (1 - partiallyThere) * bottomStackPeekSize;
435 setTranslationY(translationY - mTopPadding);
436 stackHeight = (int) (height - (translationY - mTopPadding));
437 }
438 if (stackHeight != mCurrentStackHeight) {
439 mCurrentStackHeight = stackHeight;
440 updateAlgorithmHeightAndPadding();
Selim Cinek319bdc42014-05-01 23:01:58 +0200441 requestChildrenUpdate();
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200442 }
Selim Cinek67b22602014-03-10 15:40:16 +0100443 }
444
445 /**
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100446 * Get the current height of the view. This is at most the msize of the view given by a the
Selim Cinek67b22602014-03-10 15:40:16 +0100447 * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
448 *
449 * @return either the layout height or the externally defined height, whichever is smaller
450 */
Selim Cinek343e6e22014-04-11 21:23:30 +0200451 private int getLayoutHeight() {
Selim Cinek67b22602014-03-10 15:40:16 +0100452 return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
453 }
454
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100455 public int getItemHeight() {
456 return mCollapsedSize;
457 }
458
459 public int getBottomStackPeekSize() {
460 return mBottomStackPeekSize;
461 }
462
Selim Cinek67b22602014-03-10 15:40:16 +0100463 public void setLongPressListener(View.OnLongClickListener listener) {
464 mSwipeHelper.setLongPressListener(listener);
Selim Cinek56109b02014-06-04 16:47:54 +0200465 mLongClickListener = listener;
Selim Cinek67b22602014-03-10 15:40:16 +0100466 }
467
468 public void onChildDismissed(View v) {
469 if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
470 final View veto = v.findViewById(R.id.veto);
471 if (veto != null && veto.getVisibility() != View.GONE) {
472 veto.performClick();
473 }
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100474 setSwipingInProgress(false);
Selim Cinekeb973562014-05-02 17:07:49 +0200475 if (mDragAnimPendingChildren.contains(v)) {
476 // We start the swipe and finish it in the same frame, we don't want any animation
477 // for the drag
478 mDragAnimPendingChildren.remove(v);
479 }
Selim Cinek572bbd42014-04-25 16:43:27 +0200480 mSwipedOutViews.add(v);
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200481 mAmbientState.onDragFinished(v);
Selim Cinekeb973562014-05-02 17:07:49 +0200482 }
483
484 @Override
485 public void onChildSnappedBack(View animView) {
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200486 mAmbientState.onDragFinished(animView);
Selim Cinekeb973562014-05-02 17:07:49 +0200487 if (!mDragAnimPendingChildren.contains(animView)) {
Jorim Jaggi75c95042014-05-16 19:09:59 +0200488 if (mAnimationsEnabled) {
489 mSnappedBackChildren.add(animView);
490 mNeedsAnimation = true;
491 }
Selim Cinekeb973562014-05-02 17:07:49 +0200492 requestChildrenUpdate();
Selim Cinekeb973562014-05-02 17:07:49 +0200493 } else {
494 // We start the swipe and snap back in the same frame, we don't want any animation
495 mDragAnimPendingChildren.remove(animView);
496 }
Selim Cinek67b22602014-03-10 15:40:16 +0100497 }
498
Adrian Roos5d9cc662014-05-28 17:08:13 +0200499 @Override
500 public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
501 return false;
502 }
503
Selim Cinek67b22602014-03-10 15:40:16 +0100504 public void onBeginDrag(View v) {
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100505 setSwipingInProgress(true);
Jorim Jaggid552d9d2014-05-07 19:41:13 +0200506 mAmbientState.onBeginDrag(v);
Jorim Jaggi75c95042014-05-16 19:09:59 +0200507 if (mAnimationsEnabled) {
508 mDragAnimPendingChildren.add(v);
509 mNeedsAnimation = true;
510 }
Selim Cinekeb973562014-05-02 17:07:49 +0200511 requestChildrenUpdate();
Selim Cinek67b22602014-03-10 15:40:16 +0100512 }
513
514 public void onDragCancelled(View v) {
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100515 setSwipingInProgress(false);
Selim Cinek67b22602014-03-10 15:40:16 +0100516 }
517
518 public View getChildAtPosition(MotionEvent ev) {
519 return getChildAtPosition(ev.getX(), ev.getY());
520 }
521
Jorim Jaggi4222d9a2014-04-23 16:13:15 +0200522 public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
Selim Cinek67b22602014-03-10 15:40:16 +0100523 int[] location = new int[2];
524 getLocationOnScreen(location);
Selim Cinek319bdc42014-05-01 23:01:58 +0200525 return getChildAtPosition(touchX - location[0], touchY - location[1]);
Selim Cinek67b22602014-03-10 15:40:16 +0100526 }
527
Jorim Jaggi4222d9a2014-04-23 16:13:15 +0200528 public ExpandableView getChildAtPosition(float touchX, float touchY) {
Selim Cinek67b22602014-03-10 15:40:16 +0100529 // find the view under the pointer, accounting for GONE views
530 final int count = getChildCount();
531 for (int childIdx = 0; childIdx < count; childIdx++) {
Jorim Jaggibe565df2014-04-28 17:51:23 +0200532 ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
Selim Cinek67b22602014-03-10 15:40:16 +0100533 if (slidingChild.getVisibility() == GONE) {
534 continue;
535 }
Selim Cinek89faff12014-06-19 16:29:04 -0700536 float childTop = slidingChild.getTranslationY();
537 float top = childTop + slidingChild.getClipTopAmount();
538 float bottom = childTop + slidingChild.getActualHeight();
Selim Cinek67b22602014-03-10 15:40:16 +0100539 int left = slidingChild.getLeft();
540 int right = slidingChild.getRight();
541
542 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
543 return slidingChild;
544 }
545 }
546 return null;
547 }
548
549 public boolean canChildBeExpanded(View v) {
550 return v instanceof ExpandableNotificationRow
551 && ((ExpandableNotificationRow) v).isExpandable();
552 }
553
554 public void setUserExpandedChild(View v, boolean userExpanded) {
555 if (v instanceof ExpandableNotificationRow) {
556 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
557 }
558 }
559
560 public void setUserLockedChild(View v, boolean userLocked) {
561 if (v instanceof ExpandableNotificationRow) {
562 ((ExpandableNotificationRow) v).setUserLocked(userLocked);
563 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200564 removeLongPressCallback();
565 requestDisallowInterceptTouchEvent(true);
566 }
567
568 @Override
569 public void expansionStateChanged(boolean isExpanding) {
570 mExpandingNotification = isExpanding;
571 if (!mExpandedInThisMotion) {
572 mMaxScrollAfterExpand = mOwnScrollY;
573 mExpandedInThisMotion = true;
574 }
575 }
576
577 public void setScrollingEnabled(boolean enable) {
578 mScrollingEnabled = enable;
579 }
580
581 public void setExpandingEnabled(boolean enable) {
582 mExpandHelper.setEnabled(enable);
583 }
584
585 private boolean isScrollingEnabled() {
586 return mScrollingEnabled;
Selim Cinek67b22602014-03-10 15:40:16 +0100587 }
588
589 public View getChildContentView(View v) {
590 return v;
591 }
592
593 public boolean canChildBeDismissed(View v) {
594 final View veto = v.findViewById(R.id.veto);
595 return (veto != null && veto.getVisibility() != View.GONE);
596 }
597
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100598 private void setSwipingInProgress(boolean isSwiped) {
599 mSwipingInProgress = isSwiped;
600 if(isSwiped) {
601 requestDisallowInterceptTouchEvent(true);
602 }
Selim Cinek67b22602014-03-10 15:40:16 +0100603 }
604
605 @Override
606 protected void onConfigurationChanged(Configuration newConfig) {
607 super.onConfigurationChanged(newConfig);
608 float densityScale = getResources().getDisplayMetrics().density;
609 mSwipeHelper.setDensityScale(densityScale);
610 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
611 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
612 initView(getContext());
613 }
614
615 public void dismissRowAnimated(View child, int vel) {
616 mSwipeHelper.dismissChild(child, vel);
617 }
618
619 @Override
620 public boolean onTouchEvent(MotionEvent ev) {
Jorim Jaggid7daab72014-05-06 22:22:20 +0200621 if (!isEnabled()) {
622 return false;
623 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200624 boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
625 || ev.getActionMasked()== MotionEvent.ACTION_UP;
626 boolean expandWantsIt = false;
627 if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) {
628 if (isCancelOrUp) {
629 mExpandHelper.onlyObserveMovements(false);
630 }
631 boolean wasExpandingBefore = mExpandingNotification;
632 expandWantsIt = mExpandHelper.onTouchEvent(ev);
633 if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore) {
634 dispatchDownEventToScroller(ev);
635 }
636 }
Selim Cinek67b22602014-03-10 15:40:16 +0100637 boolean scrollerWantsIt = false;
Selim Cinek1408eb52014-06-02 14:45:38 +0200638 if (!mSwipingInProgress && !mExpandingNotification) {
Selim Cinek67b22602014-03-10 15:40:16 +0100639 scrollerWantsIt = onScrollTouch(ev);
640 }
641 boolean horizontalSwipeWantsIt = false;
Selim Cinek1408eb52014-06-02 14:45:38 +0200642 if (!mIsBeingDragged
643 && !mExpandingNotification
644 && !mExpandedInThisMotion
645 && !mOnlyScrollingInThisMotion) {
Selim Cinek67b22602014-03-10 15:40:16 +0100646 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
647 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200648 return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
649 }
650
651 private void dispatchDownEventToScroller(MotionEvent ev) {
652 MotionEvent downEvent = MotionEvent.obtain(ev);
653 downEvent.setAction(MotionEvent.ACTION_DOWN);
654 onScrollTouch(downEvent);
655 downEvent.recycle();
Selim Cinek67b22602014-03-10 15:40:16 +0100656 }
657
658 private boolean onScrollTouch(MotionEvent ev) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200659 if (!isScrollingEnabled()) {
660 return false;
661 }
Selim Cinek67b22602014-03-10 15:40:16 +0100662 initVelocityTrackerIfNotExists();
663 mVelocityTracker.addMovement(ev);
664
665 final int action = ev.getAction();
666
667 switch (action & MotionEvent.ACTION_MASK) {
668 case MotionEvent.ACTION_DOWN: {
Jorim Jaggife6bfa62014-05-07 23:23:18 +0200669 if (getChildCount() == 0 || !isInContentBounds(ev)) {
Selim Cinek67b22602014-03-10 15:40:16 +0100670 return false;
671 }
672 boolean isBeingDragged = !mScroller.isFinished();
673 setIsBeingDragged(isBeingDragged);
Selim Cinek67b22602014-03-10 15:40:16 +0100674
675 /*
676 * If being flinged and user touches, stop the fling. isFinished
677 * will be false if being flinged.
678 */
679 if (!mScroller.isFinished()) {
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200680 mScroller.forceFinished(true);
Selim Cinek67b22602014-03-10 15:40:16 +0100681 }
682
683 // Remember where the motion event started
684 mLastMotionY = (int) ev.getY();
Selim Cinek1408eb52014-06-02 14:45:38 +0200685 mDownX = (int) ev.getX();
Selim Cinek67b22602014-03-10 15:40:16 +0100686 mActivePointerId = ev.getPointerId(0);
687 break;
688 }
689 case MotionEvent.ACTION_MOVE:
690 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
691 if (activePointerIndex == -1) {
692 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
693 break;
694 }
695
696 final int y = (int) ev.getY(activePointerIndex);
Selim Cinek1408eb52014-06-02 14:45:38 +0200697 final int x = (int) ev.getX(activePointerIndex);
Selim Cinek67b22602014-03-10 15:40:16 +0100698 int deltaY = mLastMotionY - y;
Selim Cinek1408eb52014-06-02 14:45:38 +0200699 final int xDiff = Math.abs(x - mDownX);
700 final int yDiff = Math.abs(deltaY);
701 if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) {
Selim Cinek67b22602014-03-10 15:40:16 +0100702 setIsBeingDragged(true);
703 if (deltaY > 0) {
704 deltaY -= mTouchSlop;
705 } else {
706 deltaY += mTouchSlop;
707 }
708 }
709 if (mIsBeingDragged) {
710 // Scroll to follow the motion event
711 mLastMotionY = y;
Selim Cinek1408eb52014-06-02 14:45:38 +0200712 int range = getScrollRange();
713 if (mExpandedInThisMotion) {
714 range = Math.min(range, mMaxScrollAfterExpand);
715 }
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200716
717 float scrollAmount;
718 if (deltaY < 0) {
719 scrollAmount = overScrollDown(deltaY);
720 } else {
721 scrollAmount = overScrollUp(deltaY, range);
722 }
Selim Cinek67b22602014-03-10 15:40:16 +0100723
724 // Calling overScrollBy will call onOverScrolled, which
725 // calls onScrollChanged if applicable.
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200726 if (scrollAmount != 0.0f) {
727 // The scrolling motion could not be compensated with the
728 // existing overScroll, we have to scroll the view
729 overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY,
730 0, range, 0, getHeight() / 2, true);
Selim Cinek67b22602014-03-10 15:40:16 +0100731 }
Selim Cinek67b22602014-03-10 15:40:16 +0100732 }
733 break;
734 case MotionEvent.ACTION_UP:
735 if (mIsBeingDragged) {
736 final VelocityTracker velocityTracker = mVelocityTracker;
737 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
738 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
739
Selim Cinek1408eb52014-06-02 14:45:38 +0200740 if (shouldOverScrollFling(initialVelocity)) {
741 onOverScrollFling(true, initialVelocity);
742 } else {
743 if (getChildCount() > 0) {
744 if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
745 float currentOverScrollTop = getCurrentOverScrollAmount(true);
746 if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
747 fling(-initialVelocity);
748 } else {
749 onOverScrollFling(false, initialVelocity);
750 }
751 } else {
752 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
753 getScrollRange())) {
754 postInvalidateOnAnimation();
755 }
Selim Cinek67b22602014-03-10 15:40:16 +0100756 }
757 }
Selim Cinek1408eb52014-06-02 14:45:38 +0200758 }
Selim Cinek48e746c2014-06-16 16:01:03 -0700759
760 mActivePointerId = INVALID_POINTER;
761 endDrag();
Selim Cinek67b22602014-03-10 15:40:16 +0100762 }
Selim Cinek48e746c2014-06-16 16:01:03 -0700763
Selim Cinek67b22602014-03-10 15:40:16 +0100764 break;
765 case MotionEvent.ACTION_CANCEL:
766 if (mIsBeingDragged && getChildCount() > 0) {
767 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
768 postInvalidateOnAnimation();
769 }
770 mActivePointerId = INVALID_POINTER;
771 endDrag();
772 }
773 break;
774 case MotionEvent.ACTION_POINTER_DOWN: {
775 final int index = ev.getActionIndex();
776 mLastMotionY = (int) ev.getY(index);
Selim Cinek1408eb52014-06-02 14:45:38 +0200777 mDownX = (int) ev.getX(index);
Selim Cinek67b22602014-03-10 15:40:16 +0100778 mActivePointerId = ev.getPointerId(index);
779 break;
780 }
781 case MotionEvent.ACTION_POINTER_UP:
782 onSecondaryPointerUp(ev);
783 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
Selim Cinek1408eb52014-06-02 14:45:38 +0200784 mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
Selim Cinek67b22602014-03-10 15:40:16 +0100785 break;
786 }
787 return true;
788 }
789
Selim Cinek1408eb52014-06-02 14:45:38 +0200790 private void onOverScrollFling(boolean open, int initialVelocity) {
791 if (mOverscrollTopChangedListener != null) {
792 mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
793 }
794 mDontReportNextOverScroll = true;
795 setOverScrollAmount(0.0f, true, false);
796 }
797
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200798 /**
799 * Perform a scroll upwards and adapt the overscroll amounts accordingly
800 *
801 * @param deltaY The amount to scroll upwards, has to be positive.
802 * @return The amount of scrolling to be performed by the scroller,
803 * not handled by the overScroll amount.
804 */
805 private float overScrollUp(int deltaY, int range) {
806 deltaY = Math.max(deltaY, 0);
807 float currentTopAmount = getCurrentOverScrollAmount(true);
808 float newTopAmount = currentTopAmount - deltaY;
809 if (currentTopAmount > 0) {
810 setOverScrollAmount(newTopAmount, true /* onTop */,
811 false /* animate */);
812 }
813 // Top overScroll might not grab all scrolling motion,
814 // we have to scroll as well.
815 float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
816 float newScrollY = mOwnScrollY + scrollAmount;
817 if (newScrollY > range) {
Selim Cinek1408eb52014-06-02 14:45:38 +0200818 if (!mExpandedInThisMotion) {
819 float currentBottomPixels = getCurrentOverScrolledPixels(false);
820 // We overScroll on the top
821 setOverScrolledPixels(currentBottomPixels + newScrollY - range,
822 false /* onTop */,
823 false /* animate */);
824 }
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200825 mOwnScrollY = range;
826 scrollAmount = 0.0f;
827 }
828 return scrollAmount;
829 }
830
831 /**
832 * Perform a scroll downward and adapt the overscroll amounts accordingly
833 *
834 * @param deltaY The amount to scroll downwards, has to be negative.
835 * @return The amount of scrolling to be performed by the scroller,
836 * not handled by the overScroll amount.
837 */
838 private float overScrollDown(int deltaY) {
839 deltaY = Math.min(deltaY, 0);
840 float currentBottomAmount = getCurrentOverScrollAmount(false);
841 float newBottomAmount = currentBottomAmount + deltaY;
842 if (currentBottomAmount > 0) {
843 setOverScrollAmount(newBottomAmount, false /* onTop */,
844 false /* animate */);
845 }
846 // Bottom overScroll might not grab all scrolling motion,
847 // we have to scroll as well.
848 float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
849 float newScrollY = mOwnScrollY + scrollAmount;
850 if (newScrollY < 0) {
851 float currentTopPixels = getCurrentOverScrolledPixels(true);
852 // We overScroll on the top
853 setOverScrolledPixels(currentTopPixels - newScrollY,
854 true /* onTop */,
855 false /* animate */);
856 mOwnScrollY = 0;
857 scrollAmount = 0.0f;
858 }
859 return scrollAmount;
860 }
861
Selim Cinek67b22602014-03-10 15:40:16 +0100862 private void onSecondaryPointerUp(MotionEvent ev) {
863 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
864 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
865 final int pointerId = ev.getPointerId(pointerIndex);
866 if (pointerId == mActivePointerId) {
867 // This was our active pointer going up. Choose a new
868 // active pointer and adjust accordingly.
869 // TODO: Make this decision more intelligent.
870 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
871 mLastMotionY = (int) ev.getY(newPointerIndex);
872 mActivePointerId = ev.getPointerId(newPointerIndex);
873 if (mVelocityTracker != null) {
874 mVelocityTracker.clear();
875 }
876 }
877 }
878
879 private void initVelocityTrackerIfNotExists() {
880 if (mVelocityTracker == null) {
881 mVelocityTracker = VelocityTracker.obtain();
882 }
883 }
884
885 private void recycleVelocityTracker() {
886 if (mVelocityTracker != null) {
887 mVelocityTracker.recycle();
888 mVelocityTracker = null;
889 }
890 }
891
892 private void initOrResetVelocityTracker() {
893 if (mVelocityTracker == null) {
894 mVelocityTracker = VelocityTracker.obtain();
895 } else {
896 mVelocityTracker.clear();
897 }
898 }
899
900 @Override
901 public void computeScroll() {
902 if (mScroller.computeScrollOffset()) {
903 // This is called at drawing time by ViewGroup.
904 int oldX = mScrollX;
905 int oldY = mOwnScrollY;
906 int x = mScroller.getCurrX();
907 int y = mScroller.getCurrY();
908
909 if (oldX != x || oldY != y) {
910 final int range = getScrollRange();
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200911 if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
912 float currVelocity = mScroller.getCurrVelocity();
Selim Cinekba819622014-05-13 17:55:53 +0200913 if (currVelocity >= mMinimumVelocity) {
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200914 mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
915 }
916 }
Selim Cinek67b22602014-03-10 15:40:16 +0100917
918 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200919 0, (int) (mMaxOverScroll), false);
Selim Cinek67b22602014-03-10 15:40:16 +0100920 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
Selim Cinek67b22602014-03-10 15:40:16 +0100921 }
922
923 // Keep on drawing until the animation has finished.
924 postInvalidateOnAnimation();
925 }
926 }
927
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200928 @Override
Selim Cinek4195dd02014-05-19 18:16:14 +0200929 protected boolean overScrollBy(int deltaX, int deltaY,
930 int scrollX, int scrollY,
931 int scrollRangeX, int scrollRangeY,
932 int maxOverScrollX, int maxOverScrollY,
933 boolean isTouchEvent) {
934
935 int newScrollY = scrollY + deltaY;
936
937 final int top = -maxOverScrollY;
938 final int bottom = maxOverScrollY + scrollRangeY;
939
940 boolean clampedY = false;
941 if (newScrollY > bottom) {
942 newScrollY = bottom;
943 clampedY = true;
944 } else if (newScrollY < top) {
945 newScrollY = top;
946 clampedY = true;
947 }
948
949 onOverScrolled(0, newScrollY, false, clampedY);
950
951 return clampedY;
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200952 }
953
954 /**
955 * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
956 * overscroll effect based on numPixels. By default this will also cancel animations on the
957 * same overScroll edge.
958 *
959 * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
960 * the rubber-banding logic.
961 * @param onTop Should the effect be applied on top of the scroller.
962 * @param animate Should an animation be performed.
963 */
964 public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
Selim Cinekfed1ab62014-06-17 14:10:33 -0700965 setOverScrollAmount(numPixels * getRubberBandFactor(onTop), onTop, animate, true);
Selim Cinek8d9ff9c2014-05-12 15:13:04 +0200966 }
967
968 /**
969 * Set the effective overScroll amount which will be directly reflected in the layout.
970 * By default this will also cancel animations on the same overScroll edge.
971 *
972 * @param amount The amount to overScroll by.
973 * @param onTop Should the effect be applied on top of the scroller.
974 * @param animate Should an animation be performed.
975 */
976 public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
977 setOverScrollAmount(amount, onTop, animate, true);
978 }
979
980 /**
981 * Set the effective overScroll amount which will be directly reflected in the layout.
982 *
983 * @param amount The amount to overScroll by.
984 * @param onTop Should the effect be applied on top of the scroller.
985 * @param animate Should an animation be performed.
986 * @param cancelAnimators Should running animations be cancelled.
987 */
988 public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
989 boolean cancelAnimators) {
Jorim Jaggi475b21d2014-07-01 18:13:24 +0200990 setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop));
991 }
992
993 /**
994 * Set the effective overScroll amount which will be directly reflected in the layout.
995 *
996 * @param amount The amount to overScroll by.
997 * @param onTop Should the effect be applied on top of the scroller.
998 * @param animate Should an animation be performed.
999 * @param cancelAnimators Should running animations be cancelled.
1000 * @param isRubberbanded The value which will be passed to
1001 * {@link OnOverscrollTopChangedListener#onOverscrollTopChanged}
1002 */
1003 public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
1004 boolean cancelAnimators, boolean isRubberbanded) {
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001005 if (cancelAnimators) {
1006 mStateAnimator.cancelOverScrollAnimators(onTop);
1007 }
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001008 setOverScrollAmountInternal(amount, onTop, animate, isRubberbanded);
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001009 }
1010
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001011 private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate,
1012 boolean isRubberbanded) {
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001013 amount = Math.max(0, amount);
1014 if (animate) {
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001015 mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded);
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001016 } else {
Selim Cinekfed1ab62014-06-17 14:10:33 -07001017 setOverScrolledPixels(amount / getRubberBandFactor(onTop), onTop);
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001018 mAmbientState.setOverScrollAmount(amount, onTop);
Jorim Jaggi290600a2014-05-30 17:02:20 +02001019 if (onTop) {
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001020 notifyOverscrollTopListener(amount, isRubberbanded);
Jorim Jaggi290600a2014-05-30 17:02:20 +02001021 }
Selim Cinek1408eb52014-06-02 14:45:38 +02001022 requestChildrenUpdate();
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001023 }
1024 }
1025
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001026 private void notifyOverscrollTopListener(float amount, boolean isRubberbanded) {
Selim Cinek1408eb52014-06-02 14:45:38 +02001027 mExpandHelper.onlyObserveMovements(amount > 1.0f);
1028 if (mDontReportNextOverScroll) {
1029 mDontReportNextOverScroll = false;
1030 return;
1031 }
Jorim Jaggi290600a2014-05-30 17:02:20 +02001032 if (mOverscrollTopChangedListener != null) {
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001033 mOverscrollTopChangedListener.onOverscrollTopChanged(amount, isRubberbanded);
Jorim Jaggi290600a2014-05-30 17:02:20 +02001034 }
1035 }
1036
1037 public void setOverscrollTopChangedListener(
1038 OnOverscrollTopChangedListener overscrollTopChangedListener) {
1039 mOverscrollTopChangedListener = overscrollTopChangedListener;
1040 }
1041
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001042 public float getCurrentOverScrollAmount(boolean top) {
1043 return mAmbientState.getOverScrollAmount(top);
1044 }
1045
1046 public float getCurrentOverScrolledPixels(boolean top) {
1047 return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
1048 }
1049
1050 private void setOverScrolledPixels(float amount, boolean onTop) {
1051 if (onTop) {
1052 mOverScrolledTopPixels = amount;
1053 } else {
1054 mOverScrolledBottomPixels = amount;
1055 }
1056 }
1057
Selim Cinek319bdc42014-05-01 23:01:58 +02001058 private void customScrollTo(int y) {
Selim Cinek67b22602014-03-10 15:40:16 +01001059 mOwnScrollY = y;
Selim Cinek3af00cf2014-05-07 17:27:26 +02001060 updateChildren();
Selim Cinek67b22602014-03-10 15:40:16 +01001061 }
1062
1063 @Override
Selim Cinek319bdc42014-05-01 23:01:58 +02001064 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
Selim Cinek67b22602014-03-10 15:40:16 +01001065 // Treat animating scrolls differently; see #computeScroll() for why.
1066 if (!mScroller.isFinished()) {
1067 final int oldX = mScrollX;
1068 final int oldY = mOwnScrollY;
1069 mScrollX = scrollX;
1070 mOwnScrollY = scrollY;
Selim Cinek67b22602014-03-10 15:40:16 +01001071 if (clampedY) {
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001072 springBack();
1073 } else {
1074 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
1075 invalidateParentIfNeeded();
1076 updateChildren();
Jorim Jaggi290600a2014-05-30 17:02:20 +02001077 float overScrollTop = getCurrentOverScrollAmount(true);
1078 if (mOwnScrollY < 0) {
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001079 notifyOverscrollTopListener(-mOwnScrollY, isRubberbanded(true));
Jorim Jaggi290600a2014-05-30 17:02:20 +02001080 } else {
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001081 notifyOverscrollTopListener(overScrollTop, isRubberbanded(true));
Jorim Jaggi290600a2014-05-30 17:02:20 +02001082 }
Selim Cinek67b22602014-03-10 15:40:16 +01001083 }
Selim Cinek67b22602014-03-10 15:40:16 +01001084 } else {
1085 customScrollTo(scrollY);
1086 scrollTo(scrollX, mScrollY);
1087 }
1088 }
1089
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001090 private void springBack() {
1091 int scrollRange = getScrollRange();
1092 boolean overScrolledTop = mOwnScrollY <= 0;
1093 boolean overScrolledBottom = mOwnScrollY >= scrollRange;
1094 if (overScrolledTop || overScrolledBottom) {
1095 boolean onTop;
1096 float newAmount;
1097 if (overScrolledTop) {
1098 onTop = true;
1099 newAmount = -mOwnScrollY;
1100 mOwnScrollY = 0;
Selim Cinek1408eb52014-06-02 14:45:38 +02001101 mDontReportNextOverScroll = true;
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001102 } else {
1103 onTop = false;
1104 newAmount = mOwnScrollY - scrollRange;
1105 mOwnScrollY = scrollRange;
1106 }
1107 setOverScrollAmount(newAmount, onTop, false);
1108 setOverScrollAmount(0.0f, onTop, true);
1109 mScroller.forceFinished(true);
1110 }
1111 }
1112
Selim Cinek67b22602014-03-10 15:40:16 +01001113 private int getScrollRange() {
1114 int scrollRange = 0;
Jorim Jaggibe565df2014-04-28 17:51:23 +02001115 ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
Selim Cinek343e6e22014-04-11 21:23:30 +02001116 if (firstChild != null) {
Selim Cinek67b22602014-03-10 15:40:16 +01001117 int contentHeight = getContentHeight();
Selim Cinek343e6e22014-04-11 21:23:30 +02001118 int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
Selim Cineka5eaa602014-05-12 21:27:47 +02001119 scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize
1120 + mBottomStackSlowDownHeight);
Selim Cinek4a1ac842014-05-01 15:51:58 +02001121 if (scrollRange > 0) {
1122 View lastChild = getLastChildNotGone();
Selim Cinek343e6e22014-04-11 21:23:30 +02001123 // We want to at least be able collapse the first item and not ending in a weird
1124 // end state.
1125 scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize);
1126 }
Selim Cinek67b22602014-03-10 15:40:16 +01001127 }
1128 return scrollRange;
1129 }
1130
Selim Cinek343e6e22014-04-11 21:23:30 +02001131 /**
1132 * @return the first child which has visibility unequal to GONE
1133 */
1134 private View getFirstChildNotGone() {
1135 int childCount = getChildCount();
1136 for (int i = 0; i < childCount; i++) {
1137 View child = getChildAt(i);
1138 if (child.getVisibility() != View.GONE) {
1139 return child;
1140 }
1141 }
1142 return null;
1143 }
1144
Selim Cinek4a1ac842014-05-01 15:51:58 +02001145 /**
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001146 * @return The first child which has visibility unequal to GONE which is currently below the
1147 * given translationY or equal to it.
1148 */
1149 private View getFirstChildBelowTranlsationY(float translationY) {
1150 int childCount = getChildCount();
1151 for (int i = 0; i < childCount; i++) {
1152 View child = getChildAt(i);
1153 if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) {
1154 return child;
1155 }
1156 }
1157 return null;
1158 }
1159
1160 /**
Selim Cinek4a1ac842014-05-01 15:51:58 +02001161 * @return the last child which has visibility unequal to GONE
1162 */
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001163 public View getLastChildNotGone() {
Selim Cinek4a1ac842014-05-01 15:51:58 +02001164 int childCount = getChildCount();
1165 for (int i = childCount - 1; i >= 0; i--) {
1166 View child = getChildAt(i);
1167 if (child.getVisibility() != View.GONE) {
1168 return child;
1169 }
1170 }
1171 return null;
1172 }
1173
Jorim Jaggi069cd032014-05-15 03:09:01 +02001174 /**
1175 * @return the number of children which have visibility unequal to GONE
1176 */
1177 public int getNotGoneChildCount() {
1178 int childCount = getChildCount();
1179 int count = 0;
1180 for (int i = 0; i < childCount; i++) {
1181 View child = getChildAt(i);
1182 if (child.getVisibility() != View.GONE) {
1183 count++;
1184 }
1185 }
1186 return count;
1187 }
1188
Selim Cinek343e6e22014-04-11 21:23:30 +02001189 private int getMaxExpandHeight(View view) {
1190 if (view instanceof ExpandableNotificationRow) {
1191 ExpandableNotificationRow row = (ExpandableNotificationRow) view;
Jorim Jaggi9cbadd32014-05-01 20:18:31 +02001192 return row.getIntrinsicHeight();
Selim Cinek343e6e22014-04-11 21:23:30 +02001193 }
1194 return view.getHeight();
1195 }
1196
Jorim Jaggi30c305c2014-07-01 23:34:41 +02001197 public int getContentHeight() {
Selim Cinek67b22602014-03-10 15:40:16 +01001198 return mContentHeight;
1199 }
1200
1201 private void updateContentHeight() {
1202 int height = 0;
1203 for (int i = 0; i < getChildCount(); i++) {
1204 View child = getChildAt(i);
Jorim Jaggid4a57442014-04-10 02:45:55 +02001205 if (child.getVisibility() != View.GONE) {
Jorim Jaggibe565df2014-04-28 17:51:23 +02001206 if (height != 0) {
1207 // add the padding before this element
Jorim Jaggid4a57442014-04-10 02:45:55 +02001208 height += mPaddingBetweenElements;
1209 }
Jorim Jaggibe565df2014-04-28 17:51:23 +02001210 if (child instanceof ExpandableNotificationRow) {
1211 ExpandableNotificationRow row = (ExpandableNotificationRow) child;
Jorim Jaggi9cbadd32014-05-01 20:18:31 +02001212 height += row.getIntrinsicHeight();
Jorim Jaggibe565df2014-04-28 17:51:23 +02001213 } else if (child instanceof ExpandableView) {
1214 ExpandableView expandableView = (ExpandableView) child;
1215 height += expandableView.getActualHeight();
1216 }
Selim Cinek67b22602014-03-10 15:40:16 +01001217 }
1218 }
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +02001219 mContentHeight = height + mTopPadding;
Selim Cinek67b22602014-03-10 15:40:16 +01001220 }
1221
1222 /**
1223 * Fling the scroll view
1224 *
1225 * @param velocityY The initial velocity in the Y direction. Positive
1226 * numbers mean that the finger/cursor is moving down the screen,
1227 * which means we want to scroll towards the top.
1228 */
1229 private void fling(int velocityY) {
1230 if (getChildCount() > 0) {
Selim Cinek4195dd02014-05-19 18:16:14 +02001231 int scrollRange = getScrollRange();
Selim Cinek67b22602014-03-10 15:40:16 +01001232
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001233 float topAmount = getCurrentOverScrollAmount(true);
1234 float bottomAmount = getCurrentOverScrollAmount(false);
1235 if (velocityY < 0 && topAmount > 0) {
1236 mOwnScrollY -= (int) topAmount;
Selim Cinek1408eb52014-06-02 14:45:38 +02001237 mDontReportNextOverScroll = true;
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001238 setOverScrollAmount(0, true, false);
Selim Cinekfed1ab62014-06-17 14:10:33 -07001239 mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001240 * mOverflingDistance + topAmount;
1241 } else if (velocityY > 0 && bottomAmount > 0) {
1242 mOwnScrollY += bottomAmount;
1243 setOverScrollAmount(0, false, false);
Selim Cinekfed1ab62014-06-17 14:10:33 -07001244 mMaxOverScroll = Math.abs(velocityY) / 1000f
1245 * getRubberBandFactor(false /* onTop */) * mOverflingDistance
1246 + bottomAmount;
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001247 } else {
1248 // it will be set once we reach the boundary
1249 mMaxOverScroll = 0.0f;
1250 }
1251 mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0,
Selim Cinek4195dd02014-05-19 18:16:14 +02001252 Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2);
Selim Cinek67b22602014-03-10 15:40:16 +01001253
1254 postInvalidateOnAnimation();
1255 }
1256 }
1257
Selim Cinek1408eb52014-06-02 14:45:38 +02001258 /**
1259 * @return Whether a fling performed on the top overscroll edge lead to the expanded
1260 * overScroll view (i.e QS).
1261 */
1262 private boolean shouldOverScrollFling(int initialVelocity) {
1263 float topOverScroll = getCurrentOverScrollAmount(true);
1264 return mScrolledToTopOnFirstDown
1265 && !mExpandedInThisMotion
1266 && topOverScroll > mMinTopOverScrollToEscape
1267 && initialVelocity > 0;
1268 }
1269
1270 public void updateTopPadding(float qsHeight, int scrollY, boolean animate) {
1271 float start = qsHeight - scrollY + mNotificationTopPadding;
1272 float stackHeight = getHeight() - start;
Jorim Jaggi30c305c2014-07-01 23:34:41 +02001273 int minStackHeight = getMinStackHeight();
1274 if (stackHeight <= minStackHeight) {
1275 float overflow = minStackHeight - stackHeight;
1276 stackHeight = minStackHeight;
Selim Cinek1408eb52014-06-02 14:45:38 +02001277 start = getHeight() - stackHeight;
1278 setTranslationY(overflow);
Jorim Jaggi30c305c2014-07-01 23:34:41 +02001279 mTopPaddingOverflow = overflow;
Selim Cinek1408eb52014-06-02 14:45:38 +02001280 } else {
1281 setTranslationY(0);
Jorim Jaggi30c305c2014-07-01 23:34:41 +02001282 mTopPaddingOverflow = 0;
Selim Cinek1408eb52014-06-02 14:45:38 +02001283 }
1284 setTopPadding(clampPadding((int) start), animate);
1285 }
1286
Jorim Jaggi30c305c2014-07-01 23:34:41 +02001287 public int getNotificationTopPadding() {
1288 return mNotificationTopPadding;
1289 }
1290
1291 public int getMinStackHeight() {
1292 return mCollapsedSize + mBottomStackPeekSize;
1293 }
1294
1295 public float getTopPaddingOverflow() {
1296 return mTopPaddingOverflow;
1297 }
1298
Jorim Jaggi2580a9762014-06-25 03:08:25 +02001299 public int getPeekHeight() {
1300 return mIntrinsicPadding + mCollapsedSize + mBottomStackPeekSize;
1301 }
1302
Selim Cinek1408eb52014-06-02 14:45:38 +02001303 private int clampPadding(int desiredPadding) {
1304 return Math.max(desiredPadding, mIntrinsicPadding);
1305 }
1306
Selim Cinekfed1ab62014-06-17 14:10:33 -07001307 private float getRubberBandFactor(boolean onTop) {
1308 if (!onTop) {
1309 return RUBBER_BAND_FACTOR_NORMAL;
1310 }
Jorim Jaggi47c85a32014-06-05 17:25:40 +02001311 if (mExpandedInThisMotion) {
1312 return RUBBER_BAND_FACTOR_AFTER_EXPAND;
1313 } else if (mIsExpansionChanging) {
1314 return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
1315 } else if (mScrolledToTopOnFirstDown) {
1316 return 1.0f;
1317 }
1318 return RUBBER_BAND_FACTOR_NORMAL;
Selim Cinek1408eb52014-06-02 14:45:38 +02001319 }
1320
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001321 /**
1322 * Accompanying function for {@link #getRubberBandFactor}: Returns true if the overscroll is
1323 * rubberbanded, false if it is technically an overscroll but rather a motion to expand the
1324 * overscroll view (e.g. expand QS).
1325 */
1326 private boolean isRubberbanded(boolean onTop) {
1327 return !onTop || mExpandedInThisMotion || mIsExpansionChanging
1328 || !mScrolledToTopOnFirstDown;
1329 }
1330
Selim Cinek67b22602014-03-10 15:40:16 +01001331 private void endDrag() {
1332 setIsBeingDragged(false);
1333
1334 recycleVelocityTracker();
1335
Selim Cinek8d9ff9c2014-05-12 15:13:04 +02001336 if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
1337 setOverScrollAmount(0, true /* onTop */, true /* animate */);
1338 }
1339 if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
1340 setOverScrollAmount(0, false /* onTop */, true /* animate */);
1341 }
Selim Cinek67b22602014-03-10 15:40:16 +01001342 }
1343
1344 @Override
1345 public boolean onInterceptTouchEvent(MotionEvent ev) {
Selim Cinek1408eb52014-06-02 14:45:38 +02001346 initDownStates(ev);
1347 boolean expandWantsIt = false;
1348 if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) {
1349 expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
1350 }
Selim Cinek67b22602014-03-10 15:40:16 +01001351 boolean scrollWantsIt = false;
Selim Cinek1408eb52014-06-02 14:45:38 +02001352 if (!mSwipingInProgress && !mExpandingNotification) {
Selim Cinek67b22602014-03-10 15:40:16 +01001353 scrollWantsIt = onInterceptTouchEventScroll(ev);
1354 }
1355 boolean swipeWantsIt = false;
Selim Cinek1408eb52014-06-02 14:45:38 +02001356 if (!mIsBeingDragged
1357 && !mExpandingNotification
1358 && !mExpandedInThisMotion
1359 && !mOnlyScrollingInThisMotion) {
Selim Cinek67b22602014-03-10 15:40:16 +01001360 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
1361 }
Selim Cinek1408eb52014-06-02 14:45:38 +02001362 return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev);
1363 }
1364
1365 private void initDownStates(MotionEvent ev) {
1366 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
1367 mExpandedInThisMotion = false;
1368 mOnlyScrollingInThisMotion = !mScroller.isFinished();
1369 }
Selim Cinek67b22602014-03-10 15:40:16 +01001370 }
1371
Christoph Studer068f5922014-04-08 17:43:07 -04001372 @Override
1373 protected void onViewRemoved(View child) {
1374 super.onViewRemoved(child);
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001375 mStackScrollAlgorithm.notifyChildrenChanged(this);
Selim Cinek159ffdb2014-06-04 22:24:18 +02001376 if (mChangePositionInProgress) {
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001377 // This is only a position change, don't do anything special
1378 return;
1379 }
Jorim Jaggibe565df2014-04-28 17:51:23 +02001380 ((ExpandableView) child).setOnHeightChangedListener(null);
Christoph Studer068f5922014-04-08 17:43:07 -04001381 mCurrentStackScrollState.removeViewStateForView(child);
Selim Cinek572bbd42014-04-25 16:43:27 +02001382 updateScrollStateForRemovedChild(child);
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001383 boolean animationGenerated = generateRemoveAnimation(child);
1384 if (animationGenerated && !mSwipedOutViews.contains(child)) {
1385 // Add this view to an overlay in order to ensure that it will still be temporary
1386 // drawn when removed
1387 getOverlay().add(child);
1388 }
Selim Cinekc27437b2014-05-14 10:23:33 +02001389 }
1390
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001391 /**
1392 * Generate a remove animation for a child view.
1393 *
1394 * @param child The view to generate the remove animation for.
1395 * @return Whether an animation was generated.
1396 */
1397 private boolean generateRemoveAnimation(View child) {
Jorim Jaggi75c95042014-05-16 19:09:59 +02001398 if (mIsExpanded && mAnimationsEnabled) {
Selim Cinekf4c19962014-05-01 21:55:31 +02001399 if (!mChildrenToAddAnimated.contains(child)) {
1400 // Generate Animations
1401 mChildrenToRemoveAnimated.add(child);
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001402 mNeedsAnimation = true;
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001403 return true;
Selim Cinekf4c19962014-05-01 21:55:31 +02001404 } else {
1405 mChildrenToAddAnimated.remove(child);
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001406 return false;
Selim Cinekf4c19962014-05-01 21:55:31 +02001407 }
Selim Cinek572bbd42014-04-25 16:43:27 +02001408 }
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001409 return false;
Selim Cinek572bbd42014-04-25 16:43:27 +02001410 }
1411
1412 /**
1413 * Updates the scroll position when a child was removed
1414 *
1415 * @param removedChild the removed child
1416 */
1417 private void updateScrollStateForRemovedChild(View removedChild) {
1418 int startingPosition = getPositionInLinearLayout(removedChild);
1419 int childHeight = removedChild.getHeight() + mPaddingBetweenElements;
1420 int endPosition = startingPosition + childHeight;
1421 if (endPosition <= mOwnScrollY) {
1422 // This child is fully scrolled of the top, so we have to deduct its height from the
1423 // scrollPosition
1424 mOwnScrollY -= childHeight;
1425 } else if (startingPosition < mOwnScrollY) {
1426 // This child is currently being scrolled into, set the scroll position to the start of
1427 // this child
1428 mOwnScrollY = startingPosition;
1429 }
1430 }
1431
1432 private int getPositionInLinearLayout(View requestedChild) {
1433 int position = 0;
1434 for (int i = 0; i < getChildCount(); i++) {
1435 View child = getChildAt(i);
1436 if (child == requestedChild) {
1437 return position;
1438 }
1439 if (child.getVisibility() != View.GONE) {
1440 position += child.getHeight();
1441 if (i < getChildCount()-1) {
1442 position += mPaddingBetweenElements;
1443 }
1444 }
1445 }
1446 return 0;
Selim Cinek1685e632014-04-08 02:27:49 +02001447 }
1448
1449 @Override
1450 protected void onViewAdded(View child) {
1451 super.onViewAdded(child);
1452 mStackScrollAlgorithm.notifyChildrenChanged(this);
Jorim Jaggibe565df2014-04-28 17:51:23 +02001453 ((ExpandableView) child).setOnHeightChangedListener(this);
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001454 generateAddAnimation(child);
Selim Cinek572bbd42014-04-25 16:43:27 +02001455 }
1456
Jorim Jaggi75c95042014-05-16 19:09:59 +02001457 public void setAnimationsEnabled(boolean animationsEnabled) {
1458 mAnimationsEnabled = animationsEnabled;
1459 }
1460
1461 public boolean isAddOrRemoveAnimationPending() {
1462 return mNeedsAnimation
1463 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
1464 }
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001465 /**
1466 * Generate an animation for an added child view.
1467 *
1468 * @param child The view to be added.
1469 */
Selim Cinek572bbd42014-04-25 16:43:27 +02001470 public void generateAddAnimation(View child) {
Selim Cinek159ffdb2014-06-04 22:24:18 +02001471 if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) {
Selim Cinek572bbd42014-04-25 16:43:27 +02001472 // Generate Animations
1473 mChildrenToAddAnimated.add(child);
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001474 mNeedsAnimation = true;
Selim Cinek572bbd42014-04-25 16:43:27 +02001475 }
1476 }
1477
1478 /**
1479 * Change the position of child to a new location
1480 *
1481 * @param child the view to change the position for
1482 * @param newIndex the new index
1483 */
1484 public void changeViewPosition(View child, int newIndex) {
1485 if (child != null && child.getParent() == this) {
Selim Cinek159ffdb2014-06-04 22:24:18 +02001486 mChangePositionInProgress = true;
Selim Cinekc27437b2014-05-14 10:23:33 +02001487 removeView(child);
1488 addView(child, newIndex);
Selim Cinek159ffdb2014-06-04 22:24:18 +02001489 mChangePositionInProgress = false;
1490 if (mIsExpanded && mAnimationsEnabled) {
1491 mChildrenChangingPositions.add(child);
1492 mNeedsAnimation = true;
1493 }
Selim Cinek572bbd42014-04-25 16:43:27 +02001494 }
1495 }
1496
Selim Cinekf4c19962014-05-01 21:55:31 +02001497 private void startAnimationToState() {
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001498 if (mNeedsAnimation) {
Selim Cinek572bbd42014-04-25 16:43:27 +02001499 generateChildHierarchyEvents();
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001500 mNeedsAnimation = false;
Selim Cinek572bbd42014-04-25 16:43:27 +02001501 }
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001502 if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
Selim Cinekf4c19962014-05-01 21:55:31 +02001503 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001504 mAnimationEvents.clear();
Selim Cinekf4c19962014-05-01 21:55:31 +02001505 } else {
1506 applyCurrentState();
1507 }
Selim Cinek572bbd42014-04-25 16:43:27 +02001508 }
1509
1510 private void generateChildHierarchyEvents() {
Selim Cinek572bbd42014-04-25 16:43:27 +02001511 generateChildRemovalEvents();
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001512 generateChildAdditionEvents();
1513 generatePositionChangeEvents();
Selim Cinekeb973562014-05-02 17:07:49 +02001514 generateSnapBackEvents();
1515 generateDragEvents();
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001516 generateTopPaddingEvent();
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001517 generateActivateEvent();
1518 generateDimmedEvent();
John Spurlockbf370992014-06-17 13:58:31 -04001519 generateDarkEvent();
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001520 mNeedsAnimation = false;
Selim Cinek572bbd42014-04-25 16:43:27 +02001521 }
1522
Selim Cinekeb973562014-05-02 17:07:49 +02001523 private void generateSnapBackEvents() {
1524 for (View child : mSnappedBackChildren) {
1525 mAnimationEvents.add(new AnimationEvent(child,
1526 AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
1527 }
1528 mSnappedBackChildren.clear();
1529 }
1530
1531 private void generateDragEvents() {
1532 for (View child : mDragAnimPendingChildren) {
1533 mAnimationEvents.add(new AnimationEvent(child,
1534 AnimationEvent.ANIMATION_TYPE_START_DRAG));
1535 }
1536 mDragAnimPendingChildren.clear();
1537 }
1538
Selim Cinek572bbd42014-04-25 16:43:27 +02001539 private void generateChildRemovalEvents() {
Selim Cinekeb973562014-05-02 17:07:49 +02001540 for (View child : mChildrenToRemoveAnimated) {
Selim Cinek572bbd42014-04-25 16:43:27 +02001541 boolean childWasSwipedOut = mSwipedOutViews.contains(child);
1542 int animationType = childWasSwipedOut
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001543 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
1544 : AnimationEvent.ANIMATION_TYPE_REMOVE;
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001545 AnimationEvent event = new AnimationEvent(child, animationType);
1546
1547 // we need to know the view after this one
1548 event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY());
1549 mAnimationEvents.add(event);
Selim Cinek572bbd42014-04-25 16:43:27 +02001550 }
1551 mSwipedOutViews.clear();
1552 mChildrenToRemoveAnimated.clear();
1553 }
1554
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001555 private void generatePositionChangeEvents() {
1556 for (View child : mChildrenChangingPositions) {
1557 mAnimationEvents.add(new AnimationEvent(child,
1558 AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
1559 }
1560 mChildrenChangingPositions.clear();
1561 }
1562
Selim Cinek572bbd42014-04-25 16:43:27 +02001563 private void generateChildAdditionEvents() {
Selim Cinekeb973562014-05-02 17:07:49 +02001564 for (View child : mChildrenToAddAnimated) {
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001565 mAnimationEvents.add(new AnimationEvent(child,
1566 AnimationEvent.ANIMATION_TYPE_ADD));
Selim Cinek572bbd42014-04-25 16:43:27 +02001567 }
1568 mChildrenToAddAnimated.clear();
Christoph Studer068f5922014-04-08 17:43:07 -04001569 }
1570
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001571 private void generateTopPaddingEvent() {
Jorim Jaggi98fb09c2014-05-01 22:40:56 +02001572 if (mTopPaddingNeedsAnimation) {
1573 mAnimationEvents.add(
1574 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
1575 }
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001576 mTopPaddingNeedsAnimation = false;
1577 }
1578
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001579 private void generateActivateEvent() {
1580 if (mActivateNeedsAnimation) {
1581 mAnimationEvents.add(
1582 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
1583 }
1584 mActivateNeedsAnimation = false;
1585 }
1586
1587 private void generateDimmedEvent() {
1588 if (mDimmedNeedsAnimation) {
1589 mAnimationEvents.add(
1590 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
1591 }
1592 mDimmedNeedsAnimation = false;
1593 }
1594
John Spurlockbf370992014-06-17 13:58:31 -04001595 private void generateDarkEvent() {
1596 if (mDarkNeedsAnimation) {
1597 mAnimationEvents.add(
1598 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK));
1599 }
1600 mDarkNeedsAnimation = false;
1601 }
1602
Selim Cinek67b22602014-03-10 15:40:16 +01001603 private boolean onInterceptTouchEventScroll(MotionEvent ev) {
Selim Cinek1408eb52014-06-02 14:45:38 +02001604 if (!isScrollingEnabled()) {
1605 return false;
1606 }
Selim Cinek67b22602014-03-10 15:40:16 +01001607 /*
1608 * This method JUST determines whether we want to intercept the motion.
1609 * If we return true, onMotionEvent will be called and we do the actual
1610 * scrolling there.
1611 */
1612
1613 /*
1614 * Shortcut the most recurring case: the user is in the dragging
1615 * state and he is moving his finger. We want to intercept this
1616 * motion.
1617 */
1618 final int action = ev.getAction();
1619 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
1620 return true;
1621 }
1622
Selim Cinek67b22602014-03-10 15:40:16 +01001623 switch (action & MotionEvent.ACTION_MASK) {
1624 case MotionEvent.ACTION_MOVE: {
1625 /*
1626 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1627 * whether the user has moved far enough from his original down touch.
1628 */
1629
1630 /*
1631 * Locally do absolute value. mLastMotionY is set to the y value
1632 * of the down event.
1633 */
1634 final int activePointerId = mActivePointerId;
1635 if (activePointerId == INVALID_POINTER) {
1636 // If we don't have a valid id, the touch down wasn't on content.
1637 break;
1638 }
1639
1640 final int pointerIndex = ev.findPointerIndex(activePointerId);
1641 if (pointerIndex == -1) {
1642 Log.e(TAG, "Invalid pointerId=" + activePointerId
1643 + " in onInterceptTouchEvent");
1644 break;
1645 }
1646
1647 final int y = (int) ev.getY(pointerIndex);
Selim Cinek1408eb52014-06-02 14:45:38 +02001648 final int x = (int) ev.getX(pointerIndex);
Selim Cinek67b22602014-03-10 15:40:16 +01001649 final int yDiff = Math.abs(y - mLastMotionY);
Selim Cinek1408eb52014-06-02 14:45:38 +02001650 final int xDiff = Math.abs(x - mDownX);
1651 if (yDiff > mTouchSlop && yDiff > xDiff) {
Selim Cinek67b22602014-03-10 15:40:16 +01001652 setIsBeingDragged(true);
1653 mLastMotionY = y;
Selim Cinek1408eb52014-06-02 14:45:38 +02001654 mDownX = x;
Selim Cinek67b22602014-03-10 15:40:16 +01001655 initVelocityTrackerIfNotExists();
1656 mVelocityTracker.addMovement(ev);
Selim Cinek67b22602014-03-10 15:40:16 +01001657 }
1658 break;
1659 }
1660
1661 case MotionEvent.ACTION_DOWN: {
1662 final int y = (int) ev.getY();
1663 if (getChildAtPosition(ev.getX(), y) == null) {
1664 setIsBeingDragged(false);
1665 recycleVelocityTracker();
1666 break;
1667 }
1668
1669 /*
1670 * Remember location of down touch.
1671 * ACTION_DOWN always refers to pointer index 0.
1672 */
1673 mLastMotionY = y;
Selim Cinek1408eb52014-06-02 14:45:38 +02001674 mDownX = (int) ev.getX();
Selim Cinek67b22602014-03-10 15:40:16 +01001675 mActivePointerId = ev.getPointerId(0);
Selim Cinek1408eb52014-06-02 14:45:38 +02001676 mScrolledToTopOnFirstDown = isScrolledToTop();
Selim Cinek67b22602014-03-10 15:40:16 +01001677
1678 initOrResetVelocityTracker();
1679 mVelocityTracker.addMovement(ev);
1680 /*
1681 * If being flinged and user touches the screen, initiate drag;
1682 * otherwise don't. mScroller.isFinished should be false when
1683 * being flinged.
1684 */
1685 boolean isBeingDragged = !mScroller.isFinished();
1686 setIsBeingDragged(isBeingDragged);
1687 break;
1688 }
1689
1690 case MotionEvent.ACTION_CANCEL:
1691 case MotionEvent.ACTION_UP:
1692 /* Release the drag */
1693 setIsBeingDragged(false);
1694 mActivePointerId = INVALID_POINTER;
1695 recycleVelocityTracker();
1696 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
1697 postInvalidateOnAnimation();
1698 }
1699 break;
1700 case MotionEvent.ACTION_POINTER_UP:
1701 onSecondaryPointerUp(ev);
1702 break;
1703 }
1704
1705 /*
1706 * The only time we want to intercept motion events is if we are in the
1707 * drag mode.
1708 */
1709 return mIsBeingDragged;
1710 }
1711
Jorim Jaggife6bfa62014-05-07 23:23:18 +02001712 /**
1713 * @return Whether the specified motion event is actually happening over the content.
1714 */
1715 private boolean isInContentBounds(MotionEvent event) {
1716 return event.getY() < getHeight() - getEmptyBottomMargin();
1717 }
1718
Selim Cinek67b22602014-03-10 15:40:16 +01001719 private void setIsBeingDragged(boolean isDragged) {
1720 mIsBeingDragged = isDragged;
1721 if (isDragged) {
Selim Cinekb6d85eb2014-03-28 20:21:01 +01001722 requestDisallowInterceptTouchEvent(true);
Selim Cinek1408eb52014-06-02 14:45:38 +02001723 removeLongPressCallback();
Selim Cinek67b22602014-03-10 15:40:16 +01001724 }
1725 }
1726
1727 @Override
1728 public void onWindowFocusChanged(boolean hasWindowFocus) {
1729 super.onWindowFocusChanged(hasWindowFocus);
1730 if (!hasWindowFocus) {
Selim Cinek1408eb52014-06-02 14:45:38 +02001731 removeLongPressCallback();
Selim Cinek67b22602014-03-10 15:40:16 +01001732 }
1733 }
Selim Cinekfab078b2014-03-27 22:45:58 +01001734
Selim Cinek1408eb52014-06-02 14:45:38 +02001735 public void removeLongPressCallback() {
1736 mSwipeHelper.removeLongPressCallback();
1737 }
1738
Selim Cinekfab078b2014-03-27 22:45:58 +01001739 @Override
1740 public boolean isScrolledToTop() {
1741 return mOwnScrollY == 0;
1742 }
1743
1744 @Override
Selim Cinekb6d85eb2014-03-28 20:21:01 +01001745 public boolean isScrolledToBottom() {
1746 return mOwnScrollY >= getScrollRange();
1747 }
1748
1749 @Override
Selim Cinekfab078b2014-03-27 22:45:58 +01001750 public View getHostView() {
1751 return this;
1752 }
Christoph Studer6e3eceb2014-04-01 18:40:27 +02001753
Selim Cinekb6d85eb2014-03-28 20:21:01 +01001754 public int getEmptyBottomMargin() {
Selim Cinek4a1ac842014-05-01 15:51:58 +02001755 int emptyMargin = mMaxLayoutHeight - mContentHeight;
1756 if (needsHeightAdaption()) {
Selim Cineka5eaa602014-05-12 21:27:47 +02001757 emptyMargin = emptyMargin - mBottomStackSlowDownHeight - mBottomStackPeekSize;
Jorim Jaggi1d480692014-05-20 19:41:58 +02001758 } else {
1759 emptyMargin = emptyMargin - mBottomStackPeekSize;
Selim Cinek4a1ac842014-05-01 15:51:58 +02001760 }
1761 return Math.max(emptyMargin, 0);
Selim Cinekb6d85eb2014-03-28 20:21:01 +01001762 }
1763
Selim Cinek1685e632014-04-08 02:27:49 +02001764 public void onExpansionStarted() {
Selim Cinekc27437b2014-05-14 10:23:33 +02001765 mIsExpansionChanging = true;
Selim Cinek1685e632014-04-08 02:27:49 +02001766 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState);
1767 }
1768
1769 public void onExpansionStopped() {
Selim Cinekc27437b2014-05-14 10:23:33 +02001770 mIsExpansionChanging = false;
Selim Cinek1685e632014-04-08 02:27:49 +02001771 mStackScrollAlgorithm.onExpansionStopped();
1772 }
1773
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +02001774 private void setIsExpanded(boolean isExpanded) {
Selim Cinek572bbd42014-04-25 16:43:27 +02001775 mIsExpanded = isExpanded;
Selim Cinek1685e632014-04-08 02:27:49 +02001776 mStackScrollAlgorithm.setIsExpanded(isExpanded);
1777 if (!isExpanded) {
1778 mOwnScrollY = 0;
1779 }
1780 }
1781
Jorim Jaggibe565df2014-04-28 17:51:23 +02001782 @Override
1783 public void onHeightChanged(ExpandableView view) {
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001784 updateContentHeight();
1785 updateScrollPositionIfNecessary();
Selim Cinekaef92ef2014-06-06 18:06:04 +02001786 notifyHeightChangeListener(view);
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001787 requestChildrenUpdate();
Jorim Jaggibe565df2014-04-28 17:51:23 +02001788 }
1789
1790 public void setOnHeightChangedListener(
1791 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
1792 this.mOnHeightChangedListener = mOnHeightChangedListener;
1793 }
1794
Selim Cinek572bbd42014-04-25 16:43:27 +02001795 public void onChildAnimationFinished() {
Selim Cinek319bdc42014-05-01 23:01:58 +02001796 requestChildrenUpdate();
Selim Cinek572bbd42014-04-25 16:43:27 +02001797 }
1798
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001799 /**
1800 * See {@link AmbientState#setDimmed}.
1801 */
1802 public void setDimmed(boolean dimmed, boolean animate) {
Selim Cinek34c0a8d2014-05-12 00:01:43 +02001803 mStackScrollAlgorithm.setDimmed(dimmed);
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001804 mAmbientState.setDimmed(dimmed);
Selim Cineka5eaa602014-05-12 21:27:47 +02001805 updatePadding(dimmed);
Jorim Jaggi75c95042014-05-16 19:09:59 +02001806 if (animate && mAnimationsEnabled) {
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001807 mDimmedNeedsAnimation = true;
1808 mNeedsAnimation = true;
1809 }
1810 requestChildrenUpdate();
1811 }
1812
1813 /**
1814 * See {@link AmbientState#setActivatedChild}.
1815 */
Selim Cineka32ab602014-06-11 15:06:01 +02001816 public void setActivatedChild(ActivatableNotificationView activatedChild) {
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001817 mAmbientState.setActivatedChild(activatedChild);
Jorim Jaggi75c95042014-05-16 19:09:59 +02001818 if (mAnimationsEnabled) {
1819 mActivateNeedsAnimation = true;
1820 mNeedsAnimation = true;
1821 }
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001822 requestChildrenUpdate();
1823 }
1824
Selim Cineka32ab602014-06-11 15:06:01 +02001825 public ActivatableNotificationView getActivatedChild() {
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001826 return mAmbientState.getActivatedChild();
1827 }
1828
Selim Cinek572bbd42014-04-25 16:43:27 +02001829 private void applyCurrentState() {
Selim Cinek572bbd42014-04-25 16:43:27 +02001830 mCurrentStackScrollState.apply();
Selim Cinekf4c19962014-05-01 21:55:31 +02001831 if (mListener != null) {
1832 mListener.onChildLocationsChanged(this);
1833 }
Selim Cinek572bbd42014-04-25 16:43:27 +02001834 }
1835
Selim Cinekc27437b2014-05-14 10:23:33 +02001836 public void setSpeedBumpView(SpeedBumpView speedBumpView) {
1837 mSpeedBumpView = speedBumpView;
1838 addView(speedBumpView);
1839 }
1840
1841 private void updateSpeedBump(boolean visible) {
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001842 boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE;
1843 if (visible != notGoneBefore) {
1844 int newVisibility = visible ? VISIBLE : GONE;
Selim Cinekc27437b2014-05-14 10:23:33 +02001845 mSpeedBumpView.setVisibility(newVisibility);
1846 if (visible) {
Selim Cinekc27437b2014-05-14 10:23:33 +02001847 // Make invisible to ensure that the appear animation is played.
1848 mSpeedBumpView.setInvisible();
1849 if (!mIsExpansionChanging) {
1850 generateAddAnimation(mSpeedBumpView);
1851 }
1852 } else {
1853 mSpeedBumpView.performVisibilityAnimation(false);
1854 generateRemoveAnimation(mSpeedBumpView);
1855 }
1856 }
1857 }
1858
1859 public void goToFullShade() {
1860 updateSpeedBump(true);
1861 }
1862
Selim Cinek1408eb52014-06-02 14:45:38 +02001863 public void cancelExpandHelper() {
1864 mExpandHelper.cancel();
1865 }
1866
1867 public void setIntrinsicPadding(int intrinsicPadding) {
1868 mIntrinsicPadding = intrinsicPadding;
1869 }
1870
Jorim Jaggi30c305c2014-07-01 23:34:41 +02001871 public int getIntrinsicPadding() {
1872 return mIntrinsicPadding;
1873 }
1874
Christoph Studer6e3eceb2014-04-01 18:40:27 +02001875 /**
Jorim Jaggi457cc352014-06-02 22:47:42 +02001876 * @return the y position of the first notification
1877 */
1878 public float getNotificationsTopY() {
1879 return mTopPadding + getTranslationY();
1880 }
1881
Selim Cinek5158d822014-06-04 13:20:41 +02001882 public void setTouchEnabled(boolean touchEnabled) {
1883 mTouchEnabled = touchEnabled;
1884 }
1885
1886 @Override
1887 public boolean dispatchTouchEvent(MotionEvent ev) {
1888 if (!mTouchEnabled) {
1889 return false;
1890 }
1891 return super.dispatchTouchEvent(ev);
1892 }
1893
Selim Cinekc0ce82d2014-06-10 13:21:15 +02001894 @Override
1895 public boolean shouldDelayChildPressedState() {
1896 return true;
1897 }
1898
Selim Cinekf54090e2014-06-17 17:24:51 -07001899 public void setScrimAlpha(float progress) {
1900 mAmbientState.setScrimAmount(progress);
1901 requestChildrenUpdate();
1902 }
1903
Jorim Jaggi457cc352014-06-02 22:47:42 +02001904 /**
John Spurlockbf370992014-06-17 13:58:31 -04001905 * See {@link AmbientState#setDark}.
1906 */
1907 public void setDark(boolean dark, boolean animate) {
1908 mAmbientState.setDark(dark);
1909 if (animate && mAnimationsEnabled) {
1910 mDarkNeedsAnimation = true;
1911 mNeedsAnimation = true;
1912 }
1913 requestChildrenUpdate();
1914 }
1915
1916 /**
Christoph Studer6e3eceb2014-04-01 18:40:27 +02001917 * A listener that is notified when some child locations might have changed.
1918 */
1919 public interface OnChildLocationsChangedListener {
1920 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
1921 }
Selim Cinek572bbd42014-04-25 16:43:27 +02001922
Jorim Jaggi290600a2014-05-30 17:02:20 +02001923 /**
1924 * A listener that gets notified when the overscroll at the top has changed.
1925 */
1926 public interface OnOverscrollTopChangedListener {
Jorim Jaggi475b21d2014-07-01 18:13:24 +02001927
1928 /**
1929 * Notifies a listener that the overscroll has changed.
1930 *
1931 * @param amount the amount of overscroll, in pixels
1932 * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
1933 * unrubberbanded motion to directly expand overscroll view (e.g expand
1934 * QS)
1935 */
1936 public void onOverscrollTopChanged(float amount, boolean isRubberbanded);
Selim Cinek1408eb52014-06-02 14:45:38 +02001937
1938 /**
1939 * Notify a listener that the scroller wants to escape from the scrolling motion and
1940 * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
1941 *
1942 * @param velocity The velocity that the Scroller had when over flinging
1943 * @param open Should the fling open or close the overscroll view.
1944 */
1945 public void flingTopOverscroll(float velocity, boolean open);
Jorim Jaggi290600a2014-05-30 17:02:20 +02001946 }
1947
Jorim Jaggi0dd68812014-05-01 19:17:37 +02001948 static class AnimationEvent {
Selim Cinek572bbd42014-04-25 16:43:27 +02001949
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001950 static AnimationFilter[] FILTERS = new AnimationFilter[] {
1951
1952 // ANIMATION_TYPE_ADD
1953 new AnimationFilter()
1954 .animateAlpha()
1955 .animateHeight()
Selim Cinek708a6c12014-05-28 14:16:02 +02001956 .animateTopInset()
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001957 .animateY()
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001958 .animateZ()
1959 .hasDelays(),
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001960
1961 // ANIMATION_TYPE_REMOVE
1962 new AnimationFilter()
1963 .animateAlpha()
1964 .animateHeight()
Selim Cinek708a6c12014-05-28 14:16:02 +02001965 .animateTopInset()
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001966 .animateY()
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001967 .animateZ()
1968 .hasDelays(),
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001969
1970 // ANIMATION_TYPE_REMOVE_SWIPED_OUT
1971 new AnimationFilter()
1972 .animateAlpha()
1973 .animateHeight()
Selim Cinek708a6c12014-05-28 14:16:02 +02001974 .animateTopInset()
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001975 .animateY()
Selim Cinek8efa6dd2014-05-19 16:27:37 +02001976 .animateZ()
1977 .hasDelays(),
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001978
1979 // ANIMATION_TYPE_TOP_PADDING_CHANGED
1980 new AnimationFilter()
1981 .animateAlpha()
1982 .animateHeight()
Selim Cinek708a6c12014-05-28 14:16:02 +02001983 .animateTopInset()
Jorim Jaggid552d9d2014-05-07 19:41:13 +02001984 .animateY()
1985 .animateDimmed()
1986 .animateScale()
1987 .animateZ(),
1988
1989 // ANIMATION_TYPE_START_DRAG
1990 new AnimationFilter()
1991 .animateAlpha(),
1992
1993 // ANIMATION_TYPE_SNAP_BACK
1994 new AnimationFilter()
1995 .animateAlpha(),
1996
1997 // ANIMATION_TYPE_ACTIVATED_CHILD
1998 new AnimationFilter()
1999 .animateScale()
2000 .animateAlpha(),
2001
2002 // ANIMATION_TYPE_DIMMED
2003 new AnimationFilter()
Selim Cinek34c0a8d2014-05-12 00:01:43 +02002004 .animateY()
Jorim Jaggid552d9d2014-05-07 19:41:13 +02002005 .animateScale()
Selim Cinek8efa6dd2014-05-19 16:27:37 +02002006 .animateDimmed(),
2007
2008 // ANIMATION_TYPE_CHANGE_POSITION
2009 new AnimationFilter()
2010 .animateAlpha()
2011 .animateHeight()
Selim Cinek708a6c12014-05-28 14:16:02 +02002012 .animateTopInset()
Selim Cinek8efa6dd2014-05-19 16:27:37 +02002013 .animateY()
John Spurlockbf370992014-06-17 13:58:31 -04002014 .animateZ(),
2015
2016 // ANIMATION_TYPE_DARK
2017 new AnimationFilter()
2018 .animateDark(),
Jorim Jaggid552d9d2014-05-07 19:41:13 +02002019 };
2020
Jorim Jaggi5aa045c2014-05-07 21:42:40 +02002021 static int[] LENGTHS = new int[] {
2022
2023 // ANIMATION_TYPE_ADD
Selim Cinek8efa6dd2014-05-19 16:27:37 +02002024 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
Jorim Jaggi5aa045c2014-05-07 21:42:40 +02002025
2026 // ANIMATION_TYPE_REMOVE
Selim Cinek8efa6dd2014-05-19 16:27:37 +02002027 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
Jorim Jaggi5aa045c2014-05-07 21:42:40 +02002028
2029 // ANIMATION_TYPE_REMOVE_SWIPED_OUT
2030 StackStateAnimator.ANIMATION_DURATION_STANDARD,
2031
2032 // ANIMATION_TYPE_TOP_PADDING_CHANGED
2033 StackStateAnimator.ANIMATION_DURATION_STANDARD,
2034
2035 // ANIMATION_TYPE_START_DRAG
2036 StackStateAnimator.ANIMATION_DURATION_STANDARD,
2037
2038 // ANIMATION_TYPE_SNAP_BACK
2039 StackStateAnimator.ANIMATION_DURATION_STANDARD,
2040
2041 // ANIMATION_TYPE_ACTIVATED_CHILD
2042 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
2043
2044 // ANIMATION_TYPE_DIMMED
2045 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
Selim Cinek8efa6dd2014-05-19 16:27:37 +02002046
2047 // ANIMATION_TYPE_CHANGE_POSITION
2048 StackStateAnimator.ANIMATION_DURATION_STANDARD,
John Spurlockbf370992014-06-17 13:58:31 -04002049
2050 // ANIMATION_TYPE_DARK
2051 StackStateAnimator.ANIMATION_DURATION_STANDARD,
Jorim Jaggi5aa045c2014-05-07 21:42:40 +02002052 };
2053
Selim Cinek8efa6dd2014-05-19 16:27:37 +02002054 static final int ANIMATION_TYPE_ADD = 0;
2055 static final int ANIMATION_TYPE_REMOVE = 1;
2056 static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
2057 static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
2058 static final int ANIMATION_TYPE_START_DRAG = 4;
2059 static final int ANIMATION_TYPE_SNAP_BACK = 5;
2060 static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
2061 static final int ANIMATION_TYPE_DIMMED = 7;
2062 static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
John Spurlockbf370992014-06-17 13:58:31 -04002063 static final int ANIMATION_TYPE_DARK = 9;
Jorim Jaggi0dd68812014-05-01 19:17:37 +02002064
Selim Cinek572bbd42014-04-25 16:43:27 +02002065 final long eventStartTime;
2066 final View changingView;
2067 final int animationType;
Jorim Jaggid552d9d2014-05-07 19:41:13 +02002068 final AnimationFilter filter;
Jorim Jaggi5aa045c2014-05-07 21:42:40 +02002069 final long length;
Selim Cinek8efa6dd2014-05-19 16:27:37 +02002070 View viewAfterChangingView;
Selim Cinek572bbd42014-04-25 16:43:27 +02002071
Jorim Jaggi0dd68812014-05-01 19:17:37 +02002072 AnimationEvent(View view, int type) {
Selim Cinek572bbd42014-04-25 16:43:27 +02002073 eventStartTime = AnimationUtils.currentAnimationTimeMillis();
2074 changingView = view;
2075 animationType = type;
Jorim Jaggid552d9d2014-05-07 19:41:13 +02002076 filter = FILTERS[type];
Jorim Jaggi5aa045c2014-05-07 21:42:40 +02002077 length = LENGTHS[type];
2078 }
2079
2080 /**
2081 * Combines the length of several animation events into a single value.
2082 *
2083 * @param events The events of the lengths to combine.
2084 * @return The combined length. This is just the maximum of all length.
2085 */
2086 static long combineLength(ArrayList<AnimationEvent> events) {
2087 long length = 0;
2088 int size = events.size();
2089 for (int i = 0; i < size; i++) {
2090 length = Math.max(length, events.get(i).length);
2091 }
2092 return length;
Selim Cinek572bbd42014-04-25 16:43:27 +02002093 }
2094 }
2095
Selim Cinek67b22602014-03-10 15:40:16 +01002096}