blob: f63ba9c3b07fa0d9668f9c2f9c6f92b36ebe6bb4 [file] [log] [blame]
Daniel Sandler08d05e32012-08-08 16:39:54 -04001/*
2 * Copyright (C) 2012 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.phone;
18
Jorim Jaggid7daab72014-05-06 22:22:20 +020019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
Daniel Sandler08d05e32012-08-08 16:39:54 -040022import android.content.Context;
23import android.util.AttributeSet;
Daniel Sandler040c2e42012-10-17 00:56:33 -040024import android.view.MotionEvent;
Jorim Jaggid7daab72014-05-06 22:22:20 +020025import android.view.VelocityTracker;
Daniel Sandler13522a22012-09-27 14:46:58 -040026import android.view.View;
Casey Burkhardt23b0a4e2013-04-29 12:18:32 -070027import android.view.accessibility.AccessibilityEvent;
Jorim Jaggid7daab72014-05-06 22:22:20 +020028import android.view.animation.AnimationUtils;
29import android.view.animation.Interpolator;
Daniel Sandler13522a22012-09-27 14:46:58 -040030
Chet Haase4d179dc2012-08-22 07:14:42 -070031import com.android.systemui.R;
Jorim Jaggibe565df2014-04-28 17:51:23 +020032import com.android.systemui.statusbar.ExpandableView;
Daniel Sandler151f00d2012-10-02 22:33:08 -040033import com.android.systemui.statusbar.GestureRecorder;
Jorim Jaggiecbab362014-04-23 16:13:15 +020034import com.android.systemui.statusbar.StatusBarState;
Selim Cinekb6d85eb2014-03-28 20:21:01 +010035import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
Daniel Sandler08d05e32012-08-08 16:39:54 -040036
Jorim Jaggibe565df2014-04-28 17:51:23 +020037public class NotificationPanelView extends PanelView implements
Jorim Jaggid7daab72014-05-06 22:22:20 +020038 ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
39 View.OnClickListener {
Chris Wren64161cc2012-12-17 16:49:30 -050040 public static final boolean DEBUG_GESTURES = true;
Jorim Jaggid7daab72014-05-06 22:22:20 +020041 private static final int EXPANSION_ANIMATION_LENGTH = 375;
Chet Haase4d179dc2012-08-22 07:14:42 -070042
Daniel Sandler040c2e42012-10-17 00:56:33 -040043 PhoneStatusBar mStatusBar;
Jorim Jaggid7daab72014-05-06 22:22:20 +020044 private StatusBarHeaderView mHeader;
45 private QuickSettingsContainerView mQsContainer;
John Spurlock73203eb2014-04-15 16:14:46 -040046 private View mKeyguardStatusView;
Jorim Jaggid7daab72014-05-06 22:22:20 +020047 private ObservableScrollView mScrollView;
48 private View mStackScrollerContainer;
John Spurlock73203eb2014-04-15 16:14:46 -040049
Selim Cinekb6d85eb2014-03-28 20:21:01 +010050 private NotificationStackScrollLayout mNotificationStackScroller;
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +020051 private int mNotificationTopPadding;
Jorim Jaggi98fb09c2014-05-01 22:40:56 +020052 private boolean mAnimateNextTopPaddingChange;
Chet Haase4d179dc2012-08-22 07:14:42 -070053
Jorim Jaggid7daab72014-05-06 22:22:20 +020054 private Interpolator mExpansionInterpolator;
55
56 private int mTrackingPointer;
57 private VelocityTracker mVelocityTracker;
58 private boolean mTracking;
59 private boolean mQsExpanded;
60 private float mInitialHeightOnTouch;
61 private float mInitialTouchX;
62 private float mInitialTouchY;
63 private float mQsExpansionHeight;
64 private int mQsMinExpansionHeight;
65 private int mQsMaxExpansionHeight;
66 private int mMinStackHeight;
67 private float mNotificationTranslation;
68 private int mStackScrollerIntrinsicPadding;
69 private boolean mQsExpansionEnabled = true;
70 private ValueAnimator mQsExpansionAnimator;
71
Daniel Sandler08d05e32012-08-08 16:39:54 -040072 public NotificationPanelView(Context context, AttributeSet attrs) {
73 super(context, attrs);
Daniel Sandler13522a22012-09-27 14:46:58 -040074 }
Chet Haase4d179dc2012-08-22 07:14:42 -070075
Daniel Sandler040c2e42012-10-17 00:56:33 -040076 public void setStatusBar(PhoneStatusBar bar) {
Selim Cinekb6d85eb2014-03-28 20:21:01 +010077 if (mStatusBar != null) {
78 mStatusBar.setOnFlipRunnable(null);
79 }
Daniel Sandler040c2e42012-10-17 00:56:33 -040080 mStatusBar = bar;
Selim Cinekb6d85eb2014-03-28 20:21:01 +010081 if (bar != null) {
82 mStatusBar.setOnFlipRunnable(new Runnable() {
83 @Override
84 public void run() {
85 requestPanelHeightUpdate();
86 }
87 });
88 }
Daniel Sandler040c2e42012-10-17 00:56:33 -040089 }
90
Daniel Sandler13522a22012-09-27 14:46:58 -040091 @Override
92 protected void onFinishInflate() {
93 super.onFinishInflate();
Jorim Jaggid7daab72014-05-06 22:22:20 +020094 mHeader = (StatusBarHeaderView) findViewById(R.id.header);
95 mHeader.getBackgroundView().setOnClickListener(this);
John Spurlock73203eb2014-04-15 16:14:46 -040096 mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
Jorim Jaggid7daab72014-05-06 22:22:20 +020097 mStackScrollerContainer = findViewById(R.id.notification_container_parent);
98 mQsContainer = (QuickSettingsContainerView) findViewById(R.id.quick_settings_container);
99 mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
100 mScrollView.setListener(this);
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100101 mNotificationStackScroller = (NotificationStackScrollLayout)
102 findViewById(R.id.notification_stack_scroller);
Jorim Jaggibe565df2014-04-28 17:51:23 +0200103 mNotificationStackScroller.setOnHeightChangedListener(this);
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200104 mNotificationTopPadding = getResources().getDimensionPixelSize(
105 R.dimen.notifications_top_padding);
Jorim Jaggid7daab72014-05-06 22:22:20 +0200106 mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
107 mExpansionInterpolator = AnimationUtils.loadInterpolator(
108 getContext(), android.R.interpolator.fast_out_slow_in);
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200109 }
110
111 @Override
112 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
113 super.onLayout(changed, left, top, right, bottom);
114 int keyguardBottomMargin =
115 ((MarginLayoutParams) mKeyguardStatusView.getLayoutParams()).bottomMargin;
Jorim Jaggid7daab72014-05-06 22:22:20 +0200116 if (!mQsExpanded) {
117 mStackScrollerIntrinsicPadding = mStatusBar.getBarState() == StatusBarState.KEYGUARD
118 ? mKeyguardStatusView.getBottom() + keyguardBottomMargin
119 : mHeader.getBottom() + mNotificationTopPadding;
120 mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding,
121 mAnimateNextTopPaddingChange);
122 mAnimateNextTopPaddingChange = false;
123 }
124
125 // Calculate quick setting heights.
126 mQsMinExpansionHeight = mHeader.getCollapsedHeight();
127 mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
128 if (mQsExpansionHeight == 0) {
129 mQsExpansionHeight = mQsMinExpansionHeight;
130 }
Jorim Jaggi98fb09c2014-05-01 22:40:56 +0200131 }
132
133 public void animateNextTopPaddingChange() {
134 mAnimateNextTopPaddingChange = true;
135 requestLayout();
Daniel Sandler08d05e32012-08-08 16:39:54 -0400136 }
137
Jorim Jaggid7daab72014-05-06 22:22:20 +0200138 /**
139 * @return Whether Quick Settings are currently expanded.
140 */
141 public boolean isQsExpanded() {
142 return mQsExpanded;
143 }
144
145 public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
146 mQsExpansionEnabled = qsExpansionEnabled;
147 mHeader.setExpansionEnabled(qsExpansionEnabled);
148 }
149
150 public void closeQs() {
151 cancelAnimation();
152 setQsExpansion(mQsMinExpansionHeight);
153 }
154
155 public void openQs() {
156 cancelAnimation();
157 if (mQsExpansionEnabled) {
158 setQsExpansion(mQsMaxExpansionHeight);
159 }
160 }
161
Daniel Sandler08d05e32012-08-08 16:39:54 -0400162 @Override
163 public void fling(float vel, boolean always) {
Daniel Sandler151f00d2012-10-02 22:33:08 -0400164 GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
165 if (gr != null) {
166 gr.tag(
167 "fling " + ((vel > 0) ? "open" : "closed"),
168 "notifications,v=" + vel);
169 }
Daniel Sandler08d05e32012-08-08 16:39:54 -0400170 super.fling(vel, always);
171 }
Chet Haase4d179dc2012-08-22 07:14:42 -0700172
Casey Burkhardt23b0a4e2013-04-29 12:18:32 -0700173 @Override
174 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
175 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
176 event.getText()
177 .add(getContext().getString(R.string.accessibility_desc_notification_shade));
178 return true;
179 }
180
181 return super.dispatchPopulateAccessibilityEvent(event);
182 }
183
Daniel Sandler040c2e42012-10-17 00:56:33 -0400184 @Override
John Spurlock73203eb2014-04-15 16:14:46 -0400185 public boolean onInterceptTouchEvent(MotionEvent event) {
Jorim Jaggid7daab72014-05-06 22:22:20 +0200186 int pointerIndex = event.findPointerIndex(mTrackingPointer);
187 if (pointerIndex < 0) {
188 pointerIndex = 0;
189 mTrackingPointer = event.getPointerId(pointerIndex);
John Spurlock73203eb2014-04-15 16:14:46 -0400190 }
Jorim Jaggid7daab72014-05-06 22:22:20 +0200191 final float x = event.getX(pointerIndex);
192 final float y = event.getY(pointerIndex);
193
194 switch (event.getActionMasked()) {
195 case MotionEvent.ACTION_DOWN:
196 mInitialTouchY = y;
197 mInitialTouchX = x;
198 initVelocityTracker();
199 trackMovement(event);
200 if (shouldIntercept(mInitialTouchX, mInitialTouchY, 0)) {
201 getParent().requestDisallowInterceptTouchEvent(true);
202 }
203 break;
204 case MotionEvent.ACTION_POINTER_UP:
205 final int upPointer = event.getPointerId(event.getActionIndex());
206 if (mTrackingPointer == upPointer) {
207 // gesture is ongoing, find a new pointer to track
208 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
209 mTrackingPointer = event.getPointerId(newIndex);
210 mInitialTouchX = event.getX(newIndex);
211 mInitialTouchY = event.getY(newIndex);
212 }
213 break;
214
215 case MotionEvent.ACTION_MOVE:
216 final float h = y - mInitialTouchY;
217 trackMovement(event);
218 if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
219 && shouldIntercept(mInitialTouchX, mInitialTouchY, h)) {
220 onQsExpansionStarted();
221 mInitialHeightOnTouch = mQsExpansionHeight;
222 mInitialTouchY = y;
223 mInitialTouchX = x;
224 mTracking = true;
225 return true;
226 }
227 break;
228 }
229 return !mQsExpanded && super.onInterceptTouchEvent(event);
John Spurlock73203eb2014-04-15 16:14:46 -0400230 }
231
232 @Override
Daniel Sandler040c2e42012-10-17 00:56:33 -0400233 public boolean onTouchEvent(MotionEvent event) {
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100234 // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
235 // implementation.
Jorim Jaggid7daab72014-05-06 22:22:20 +0200236 if (mTracking) {
237 int pointerIndex = event.findPointerIndex(mTrackingPointer);
238 if (pointerIndex < 0) {
239 pointerIndex = 0;
240 mTrackingPointer = event.getPointerId(pointerIndex);
241 }
242 final float y = event.getY(pointerIndex);
243 final float x = event.getX(pointerIndex);
244
245 switch (event.getActionMasked()) {
246 case MotionEvent.ACTION_DOWN:
247 mTracking = true;
248 mInitialTouchY = y;
249 mInitialTouchX = x;
250 onQsExpansionStarted();
251 mInitialHeightOnTouch = mQsExpansionHeight;
252 initVelocityTracker();
253 trackMovement(event);
254 break;
255
256 case MotionEvent.ACTION_POINTER_UP:
257 final int upPointer = event.getPointerId(event.getActionIndex());
258 if (mTrackingPointer == upPointer) {
259 // gesture is ongoing, find a new pointer to track
260 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
261 final float newY = event.getY(newIndex);
262 final float newX = event.getX(newIndex);
263 mTrackingPointer = event.getPointerId(newIndex);
264 mInitialHeightOnTouch = mQsExpansionHeight;
265 mInitialTouchY = newY;
266 mInitialTouchX = newX;
267 }
268 break;
269
270 case MotionEvent.ACTION_MOVE:
271 final float h = y - mInitialTouchY;
272 setQsExpansion(h + mInitialHeightOnTouch);
273 trackMovement(event);
274 break;
275
276 case MotionEvent.ACTION_UP:
277 case MotionEvent.ACTION_CANCEL:
278 mTracking = false;
279 mTrackingPointer = -1;
280 trackMovement(event);
281
282 float vel = getCurrentVelocity();
283
284 // TODO: Better logic whether we should expand or not.
285 flingSettings(vel, vel > 0);
286
287 if (mVelocityTracker != null) {
288 mVelocityTracker.recycle();
289 mVelocityTracker = null;
290 }
291 break;
John Spurlock73203eb2014-04-15 16:14:46 -0400292 }
293 return true;
294 }
Jorim Jaggid7daab72014-05-06 22:22:20 +0200295
296 // Consume touch events when QS are expanded.
297 return mQsExpanded || super.onTouchEvent(event);
298 }
299
300 private void onQsExpansionStarted() {
301 cancelAnimation();
302
303 // Reset scroll position and apply that position to the expanded height.
304 float height = mQsExpansionHeight - mScrollView.getScrollY();
305 mScrollView.scrollTo(0, 0);
306 setQsExpansion(height);
307 }
308
309 private void expandQs() {
310 mHeader.setExpanded(true);
311 mNotificationStackScroller.setEnabled(false);
312 mScrollView.setVisibility(View.VISIBLE);
313 mQsExpanded = true;
314 }
315
316 private void collapseQs() {
317 mHeader.setExpanded(false);
318 mNotificationStackScroller.setEnabled(true);
319 mScrollView.setVisibility(View.INVISIBLE);
320 mQsExpanded = false;
321 }
322
323 private void setQsExpansion(float height) {
324 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
325 if (height > mQsMinExpansionHeight && !mQsExpanded) {
326 expandQs();
327 } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
328 collapseQs();
John Spurlock73203eb2014-04-15 16:14:46 -0400329 }
Jorim Jaggid7daab72014-05-06 22:22:20 +0200330 mQsExpansionHeight = height;
331 mHeader.setExpansion(height);
332 setQsTranslation(height);
333 setQsStackScrollerPadding(height);
334 }
335
336 private void setQsTranslation(float height) {
337 mQsContainer.setY(height - mQsContainer.getHeight());
338 }
339
340 private void setQsStackScrollerPadding(float height) {
341 float start = height - mScrollView.getScrollY() + mNotificationTopPadding;
342 float stackHeight = mNotificationStackScroller.getHeight() - start;
343 if (stackHeight <= mMinStackHeight) {
344 float overflow = mMinStackHeight - stackHeight;
345 stackHeight = mMinStackHeight;
346 start = mNotificationStackScroller.getHeight() - stackHeight;
347 mNotificationStackScroller.setTranslationY(overflow);
348 mNotificationTranslation = overflow + mScrollView.getScrollY();
349 } else {
350 mNotificationStackScroller.setTranslationY(0);
351 mNotificationTranslation = mScrollView.getScrollY();
352 }
353 mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false);
354 }
355
356 private int clampQsStackScrollerPadding(int desiredPadding) {
357 return Math.max(desiredPadding, mStackScrollerIntrinsicPadding);
358 }
359
360 private void trackMovement(MotionEvent event) {
361 if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
362 }
363
364 private void initVelocityTracker() {
365 if (mVelocityTracker != null) {
366 mVelocityTracker.recycle();
367 }
368 mVelocityTracker = VelocityTracker.obtain();
369 }
370
371 private float getCurrentVelocity() {
372 if (mVelocityTracker == null) {
373 return 0;
374 }
375 mVelocityTracker.computeCurrentVelocity(1000);
376 return mVelocityTracker.getYVelocity();
377 }
378
379 private void cancelAnimation() {
380 if (mQsExpansionAnimator != null) {
381 mQsExpansionAnimator.cancel();
382 }
383 }
384 private void flingSettings(float vel, boolean expand) {
385
386 // TODO: Actually use velocity.
387
388 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
389 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
390 animator.setDuration(EXPANSION_ANIMATION_LENGTH);
391 animator.setInterpolator(mExpansionInterpolator);
392 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
393 @Override
394 public void onAnimationUpdate(ValueAnimator animation) {
395 setQsExpansion((Float) animation.getAnimatedValue());
396 }
397 });
398 animator.addListener(new AnimatorListenerAdapter() {
399 @Override
400 public void onAnimationEnd(Animator animation) {
401 mQsExpansionAnimator = null;
402 }
403 });
404 animator.start();
405 mQsExpansionAnimator = animator;
406 }
407
408 /**
409 * @return Whether we should intercept a gesture to open Quick Settings.
410 */
411 private boolean shouldIntercept(float x, float y, float yDiff) {
412 if (!mQsExpansionEnabled) {
413 return false;
414 }
415 View headerView = mStatusBar.getBarState() == StatusBarState.KEYGUARD && !mQsExpanded
416 ? mKeyguardStatusView
417 : mHeader;
418 boolean onHeader = x >= headerView.getLeft() && x <= headerView.getRight()
419 && y >= headerView.getTop() && y <= headerView.getBottom();
420 if (mQsExpanded) {
421 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
422 } else {
423 return onHeader;
424 }
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100425 }
426
427 @Override
428 protected boolean isScrolledToBottom() {
429 if (!isInSettings()) {
430 return mNotificationStackScroller.isScrolledToBottom();
Chris Wren64161cc2012-12-17 16:49:30 -0500431 }
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100432 return super.isScrolledToBottom();
433 }
434
435 @Override
436 protected int getMaxPanelHeight() {
437 if (!isInSettings()) {
438 int maxPanelHeight = super.getMaxPanelHeight();
Jorim Jaggid7daab72014-05-06 22:22:20 +0200439 int notificationMarginBottom = mStackScrollerContainer.getPaddingBottom();
440 int emptyBottomMargin = notificationMarginBottom
441 + mNotificationStackScroller.getEmptyBottomMargin();
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100442 return maxPanelHeight - emptyBottomMargin;
Daniel Sandler040c2e42012-10-17 00:56:33 -0400443 }
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100444 return super.getMaxPanelHeight();
445 }
446
447 private boolean isInSettings() {
Jorim Jaggid7daab72014-05-06 22:22:20 +0200448 return mQsExpanded;
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100449 }
450
451 @Override
452 protected void onHeightUpdated(float expandedHeight) {
Jorim Jaggi8c1a44b2014-04-29 19:04:02 +0200453 mNotificationStackScroller.setStackHeight(expandedHeight);
Selim Cinekb6d85eb2014-03-28 20:21:01 +0100454 }
455
456 @Override
457 protected int getDesiredMeasureHeight() {
458 return mMaxPanelHeight;
Daniel Sandler040c2e42012-10-17 00:56:33 -0400459 }
Selim Cinek1685e632014-04-08 02:27:49 +0200460
461 @Override
462 protected void onExpandingStarted() {
463 super.onExpandingStarted();
464 mNotificationStackScroller.onExpansionStarted();
465 }
466
467 @Override
468 protected void onExpandingFinished() {
469 super.onExpandingFinished();
470 mNotificationStackScroller.onExpansionStopped();
471 }
Jorim Jaggibe565df2014-04-28 17:51:23 +0200472
473 @Override
474 public void onHeightChanged(ExpandableView view) {
475 requestPanelHeightUpdate();
476 }
Jorim Jaggid7daab72014-05-06 22:22:20 +0200477
478 @Override
479 public void onScrollChanged() {
480 if (mQsExpanded) {
481 mNotificationStackScroller.setTranslationY(
482 mNotificationTranslation - mScrollView.getScrollY());
483 }
484 }
485
486 @Override
487 public void onClick(View v) {
488 if (v == mHeader.getBackgroundView()) {
489 onQsExpansionStarted();
490 if (mQsExpanded) {
491 flingSettings(0 /* vel */, false /* expand */);
492 } else if (mQsExpansionEnabled) {
493 flingSettings(0 /* vel */, true /* expand */);
494 }
495 }
496 }
Daniel Sandler08d05e32012-08-08 16:39:54 -0400497}