NotificationPanelView.java revision 87cd5e71ec087e191b8baaa8913a878f92d4e10d
1/* 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 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.util.AttributeSet; 24import android.view.MotionEvent; 25import android.view.VelocityTracker; 26import android.view.View; 27import android.view.ViewGroup; 28import android.view.accessibility.AccessibilityEvent; 29import android.view.animation.AnimationUtils; 30import android.view.animation.Interpolator; 31import android.widget.LinearLayout; 32 33import com.android.systemui.R; 34import com.android.systemui.statusbar.ExpandableView; 35import com.android.systemui.statusbar.FlingAnimationUtils; 36import com.android.systemui.statusbar.GestureRecorder; 37import com.android.systemui.statusbar.StatusBarState; 38import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 39 40public class NotificationPanelView extends PanelView implements 41 ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, 42 View.OnClickListener { 43 public static final boolean DEBUG_GESTURES = true; 44 private static final int EXPANSION_ANIMATION_LENGTH = 375; 45 46 PhoneStatusBar mStatusBar; 47 private StatusBarHeaderView mHeader; 48 private QuickSettingsContainerView mQsContainer; 49 private View mKeyguardStatusView; 50 private ObservableScrollView mScrollView; 51 private View mStackScrollerContainer; 52 53 private NotificationStackScrollLayout mNotificationStackScroller; 54 private int mNotificationTopPadding; 55 private boolean mAnimateNextTopPaddingChange; 56 57 private int mTrackingPointer; 58 private VelocityTracker mVelocityTracker; 59 private boolean mTracking; 60 61 /** 62 * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't 63 * intercepted yet. 64 */ 65 private boolean mIntercepting; 66 private boolean mQsExpanded; 67 private float mInitialHeightOnTouch; 68 private float mInitialTouchX; 69 private float mInitialTouchY; 70 private float mLastTouchX; 71 private float mLastTouchY; 72 private float mQsExpansionHeight; 73 private int mQsMinExpansionHeight; 74 private int mQsMaxExpansionHeight; 75 private int mMinStackHeight; 76 private float mNotificationTranslation; 77 private int mStackScrollerIntrinsicPadding; 78 private boolean mQsExpansionEnabled = true; 79 private ValueAnimator mQsExpansionAnimator; 80 private FlingAnimationUtils mFlingAnimationUtils; 81 82 public NotificationPanelView(Context context, AttributeSet attrs) { 83 super(context, attrs); 84 } 85 86 public void setStatusBar(PhoneStatusBar bar) { 87 if (mStatusBar != null) { 88 mStatusBar.setOnFlipRunnable(null); 89 } 90 mStatusBar = bar; 91 if (bar != null) { 92 mStatusBar.setOnFlipRunnable(new Runnable() { 93 @Override 94 public void run() { 95 requestPanelHeightUpdate(); 96 } 97 }); 98 } 99 } 100 101 @Override 102 protected void onFinishInflate() { 103 super.onFinishInflate(); 104 mHeader = (StatusBarHeaderView) findViewById(R.id.header); 105 mHeader.getBackgroundView().setOnClickListener(this); 106 mHeader.setOverlayParent(this); 107 mKeyguardStatusView = findViewById(R.id.keyguard_status_view); 108 mStackScrollerContainer = findViewById(R.id.notification_container_parent); 109 mQsContainer = (QuickSettingsContainerView) findViewById(R.id.quick_settings_container); 110 mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view); 111 mScrollView.setListener(this); 112 mNotificationStackScroller = (NotificationStackScrollLayout) 113 findViewById(R.id.notification_stack_scroller); 114 mNotificationStackScroller.setOnHeightChangedListener(this); 115 mNotificationTopPadding = getResources().getDimensionPixelSize( 116 R.dimen.notifications_top_padding); 117 mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height); 118 mFlingAnimationUtils = new FlingAnimationUtils(getContext()); 119 } 120 121 @Override 122 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 123 super.onLayout(changed, left, top, right, bottom); 124 int keyguardBottomMargin = 125 ((MarginLayoutParams) mKeyguardStatusView.getLayoutParams()).bottomMargin; 126 if (!mQsExpanded) { 127 mStackScrollerIntrinsicPadding = mStatusBar.getBarState() == StatusBarState.KEYGUARD 128 ? mKeyguardStatusView.getBottom() + keyguardBottomMargin 129 : mHeader.getBottom() + mNotificationTopPadding; 130 mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding, 131 mAnimateNextTopPaddingChange); 132 mAnimateNextTopPaddingChange = false; 133 } 134 135 // Calculate quick setting heights. 136 mQsMinExpansionHeight = mHeader.getCollapsedHeight(); 137 mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight(); 138 if (mQsExpansionHeight == 0) { 139 mQsExpansionHeight = mQsMinExpansionHeight; 140 } 141 } 142 143 public void animateNextTopPaddingChange() { 144 mAnimateNextTopPaddingChange = true; 145 requestLayout(); 146 } 147 148 /** 149 * @return Whether Quick Settings are currently expanded. 150 */ 151 public boolean isQsExpanded() { 152 return mQsExpanded; 153 } 154 155 public void setQsExpansionEnabled(boolean qsExpansionEnabled) { 156 mQsExpansionEnabled = qsExpansionEnabled; 157 } 158 159 public void closeQs() { 160 cancelAnimation(); 161 setQsExpansion(mQsMinExpansionHeight); 162 } 163 164 public void openQs() { 165 cancelAnimation(); 166 if (mQsExpansionEnabled) { 167 setQsExpansion(mQsMaxExpansionHeight); 168 } 169 } 170 171 @Override 172 public void fling(float vel, boolean always) { 173 GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); 174 if (gr != null) { 175 gr.tag( 176 "fling " + ((vel > 0) ? "open" : "closed"), 177 "notifications,v=" + vel); 178 } 179 super.fling(vel, always); 180 } 181 182 @Override 183 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 184 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 185 event.getText() 186 .add(getContext().getString(R.string.accessibility_desc_notification_shade)); 187 return true; 188 } 189 190 return super.dispatchPopulateAccessibilityEvent(event); 191 } 192 193 @Override 194 public boolean onInterceptTouchEvent(MotionEvent event) { 195 int pointerIndex = event.findPointerIndex(mTrackingPointer); 196 if (pointerIndex < 0) { 197 pointerIndex = 0; 198 mTrackingPointer = event.getPointerId(pointerIndex); 199 } 200 final float x = event.getX(pointerIndex); 201 final float y = event.getY(pointerIndex); 202 203 switch (event.getActionMasked()) { 204 case MotionEvent.ACTION_DOWN: 205 mIntercepting = true; 206 mInitialTouchY = y; 207 mInitialTouchX = x; 208 initVelocityTracker(); 209 trackMovement(event); 210 if (shouldIntercept(mInitialTouchX, mInitialTouchY, 0)) { 211 getParent().requestDisallowInterceptTouchEvent(true); 212 } 213 break; 214 case MotionEvent.ACTION_POINTER_UP: 215 final int upPointer = event.getPointerId(event.getActionIndex()); 216 if (mTrackingPointer == upPointer) { 217 // gesture is ongoing, find a new pointer to track 218 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 219 mTrackingPointer = event.getPointerId(newIndex); 220 mInitialTouchX = event.getX(newIndex); 221 mInitialTouchY = event.getY(newIndex); 222 } 223 break; 224 225 case MotionEvent.ACTION_MOVE: 226 final float h = y - mInitialTouchY; 227 trackMovement(event); 228 if (mTracking) { 229 230 // Already tracking because onOverscrolled was called. We need to update here 231 // so we don't stop for a frame until the next touch event gets handled in 232 // onTouchEvent. 233 setQsExpansion(h + mInitialHeightOnTouch); 234 trackMovement(event); 235 mIntercepting = false; 236 return true; 237 } 238 if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX) 239 && shouldIntercept(mInitialTouchX, mInitialTouchY, h)) { 240 onQsExpansionStarted(); 241 mInitialHeightOnTouch = mQsExpansionHeight; 242 mInitialTouchY = y; 243 mInitialTouchX = x; 244 mTracking = true; 245 mIntercepting = false; 246 return true; 247 } 248 break; 249 250 case MotionEvent.ACTION_CANCEL: 251 case MotionEvent.ACTION_UP: 252 mIntercepting = false; 253 break; 254 } 255 return !mQsExpanded && super.onInterceptTouchEvent(event); 256 } 257 258 @Override 259 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 260 261 // Block request so we can still intercept the scrolling when QS is expanded. 262 if (!mQsExpanded) { 263 super.requestDisallowInterceptTouchEvent(disallowIntercept); 264 } 265 } 266 267 @Override 268 public boolean onTouchEvent(MotionEvent event) { 269 // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference 270 // implementation. 271 if (mTracking) { 272 int pointerIndex = event.findPointerIndex(mTrackingPointer); 273 if (pointerIndex < 0) { 274 pointerIndex = 0; 275 mTrackingPointer = event.getPointerId(pointerIndex); 276 } 277 final float y = event.getY(pointerIndex); 278 final float x = event.getX(pointerIndex); 279 280 switch (event.getActionMasked()) { 281 case MotionEvent.ACTION_DOWN: 282 mTracking = true; 283 mInitialTouchY = y; 284 mInitialTouchX = x; 285 onQsExpansionStarted(); 286 mInitialHeightOnTouch = mQsExpansionHeight; 287 initVelocityTracker(); 288 trackMovement(event); 289 break; 290 291 case MotionEvent.ACTION_POINTER_UP: 292 final int upPointer = event.getPointerId(event.getActionIndex()); 293 if (mTrackingPointer == upPointer) { 294 // gesture is ongoing, find a new pointer to track 295 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 296 final float newY = event.getY(newIndex); 297 final float newX = event.getX(newIndex); 298 mTrackingPointer = event.getPointerId(newIndex); 299 mInitialHeightOnTouch = mQsExpansionHeight; 300 mInitialTouchY = newY; 301 mInitialTouchX = newX; 302 } 303 break; 304 305 case MotionEvent.ACTION_MOVE: 306 final float h = y - mInitialTouchY; 307 setQsExpansion(h + mInitialHeightOnTouch); 308 trackMovement(event); 309 break; 310 311 case MotionEvent.ACTION_UP: 312 case MotionEvent.ACTION_CANCEL: 313 mTracking = false; 314 mTrackingPointer = -1; 315 trackMovement(event); 316 317 float vel = getCurrentVelocity(); 318 319 // TODO: Better logic whether we should expand or not. 320 flingSettings(vel, vel > 0); 321 322 if (mVelocityTracker != null) { 323 mVelocityTracker.recycle(); 324 mVelocityTracker = null; 325 } 326 break; 327 } 328 return true; 329 } 330 331 // Consume touch events when QS are expanded. 332 return mQsExpanded || super.onTouchEvent(event); 333 } 334 335 @Override 336 public void onOverscrolled(int amount) { 337 if (mIntercepting) { 338 onQsExpansionStarted(amount); 339 mInitialHeightOnTouch = mQsExpansionHeight; 340 mInitialTouchY = mLastTouchY; 341 mInitialTouchX = mLastTouchX; 342 mTracking = true; 343 } 344 } 345 346 private void onQsExpansionStarted() { 347 onQsExpansionStarted(0); 348 } 349 350 private void onQsExpansionStarted(int overscrollAmount) { 351 cancelAnimation(); 352 353 // Reset scroll position and apply that position to the expanded height. 354 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 355 mScrollView.scrollTo(0, 0); 356 setQsExpansion(height); 357 } 358 359 private void expandQs() { 360 mHeader.setExpanded(true); 361 mNotificationStackScroller.setEnabled(false); 362 mScrollView.setVisibility(View.VISIBLE); 363 mQsExpanded = true; 364 } 365 366 private void collapseQs() { 367 mHeader.setExpanded(false); 368 mNotificationStackScroller.setEnabled(true); 369 mScrollView.setVisibility(View.INVISIBLE); 370 mQsExpanded = false; 371 } 372 373 private void setQsExpansion(float height) { 374 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); 375 if (height > mQsMinExpansionHeight && !mQsExpanded) { 376 expandQs(); 377 } else if (height <= mQsMinExpansionHeight && mQsExpanded) { 378 collapseQs(); 379 } 380 mQsExpansionHeight = height; 381 mHeader.setExpansion(height); 382 setQsTranslation(height); 383 setQsStackScrollerPadding(height); 384 } 385 386 private void setQsTranslation(float height) { 387 mQsContainer.setY(height - mQsContainer.getHeight()); 388 } 389 390 private void setQsStackScrollerPadding(float height) { 391 float start = height - mScrollView.getScrollY() + mNotificationTopPadding; 392 float stackHeight = mNotificationStackScroller.getHeight() - start; 393 if (stackHeight <= mMinStackHeight) { 394 float overflow = mMinStackHeight - stackHeight; 395 stackHeight = mMinStackHeight; 396 start = mNotificationStackScroller.getHeight() - stackHeight; 397 mNotificationStackScroller.setTranslationY(overflow); 398 mNotificationTranslation = overflow + mScrollView.getScrollY(); 399 } else { 400 mNotificationStackScroller.setTranslationY(0); 401 mNotificationTranslation = mScrollView.getScrollY(); 402 } 403 mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false); 404 } 405 406 private int clampQsStackScrollerPadding(int desiredPadding) { 407 return Math.max(desiredPadding, mStackScrollerIntrinsicPadding); 408 } 409 410 private void trackMovement(MotionEvent event) { 411 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 412 mLastTouchX = event.getX(); 413 mLastTouchY = event.getY(); 414 } 415 416 private void initVelocityTracker() { 417 if (mVelocityTracker != null) { 418 mVelocityTracker.recycle(); 419 } 420 mVelocityTracker = VelocityTracker.obtain(); 421 } 422 423 private float getCurrentVelocity() { 424 if (mVelocityTracker == null) { 425 return 0; 426 } 427 mVelocityTracker.computeCurrentVelocity(1000); 428 return mVelocityTracker.getYVelocity(); 429 } 430 431 private void cancelAnimation() { 432 if (mQsExpansionAnimator != null) { 433 mQsExpansionAnimator.cancel(); 434 } 435 } 436 private void flingSettings(float vel, boolean expand) { 437 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 438 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 439 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 440 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 441 @Override 442 public void onAnimationUpdate(ValueAnimator animation) { 443 setQsExpansion((Float) animation.getAnimatedValue()); 444 } 445 }); 446 animator.addListener(new AnimatorListenerAdapter() { 447 @Override 448 public void onAnimationEnd(Animator animation) { 449 mQsExpansionAnimator = null; 450 } 451 }); 452 animator.start(); 453 mQsExpansionAnimator = animator; 454 } 455 456 /** 457 * @return Whether we should intercept a gesture to open Quick Settings. 458 */ 459 private boolean shouldIntercept(float x, float y, float yDiff) { 460 if (!mQsExpansionEnabled) { 461 return false; 462 } 463 boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight() 464 && y >= mHeader.getTop() && y <= mHeader.getBottom(); 465 if (mQsExpanded) { 466 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0); 467 } else { 468 return onHeader; 469 } 470 } 471 472 @Override 473 public void setVisibility(int visibility) { 474 int oldVisibility = getVisibility(); 475 super.setVisibility(visibility); 476 if (visibility != oldVisibility) { 477 reparentStatusIcons(visibility == VISIBLE); 478 } 479 } 480 481 /** 482 * When the notification panel gets expanded, we need to move the status icons in the header 483 * card. 484 */ 485 private void reparentStatusIcons(boolean toHeader) { 486 if (mStatusBar == null) { 487 return; 488 } 489 LinearLayout systemIcons = mStatusBar.getSystemIcons(); 490 if (systemIcons.getParent() != null) { 491 ((ViewGroup) systemIcons.getParent()).removeView(systemIcons); 492 } 493 if (toHeader) { 494 mHeader.attachSystemIcons(systemIcons); 495 } else { 496 mHeader.onSystemIconsDetached(); 497 mStatusBar.reattachSystemIcons(); 498 } 499 } 500 501 @Override 502 protected boolean isScrolledToBottom() { 503 if (!isInSettings()) { 504 return mNotificationStackScroller.isScrolledToBottom(); 505 } 506 return super.isScrolledToBottom(); 507 } 508 509 @Override 510 protected int getMaxPanelHeight() { 511 if (!isInSettings()) { 512 int maxPanelHeight = super.getMaxPanelHeight(); 513 int notificationMarginBottom = mStackScrollerContainer.getPaddingBottom(); 514 int emptyBottomMargin = notificationMarginBottom 515 + mNotificationStackScroller.getEmptyBottomMargin(); 516 return maxPanelHeight - emptyBottomMargin; 517 } 518 return super.getMaxPanelHeight(); 519 } 520 521 private boolean isInSettings() { 522 return mQsExpanded; 523 } 524 525 @Override 526 protected void onHeightUpdated(float expandedHeight) { 527 mNotificationStackScroller.setStackHeight(expandedHeight); 528 } 529 530 @Override 531 protected int getDesiredMeasureHeight() { 532 return mMaxPanelHeight; 533 } 534 535 @Override 536 protected void onExpandingStarted() { 537 super.onExpandingStarted(); 538 mNotificationStackScroller.onExpansionStarted(); 539 } 540 541 @Override 542 protected void onExpandingFinished() { 543 super.onExpandingFinished(); 544 mNotificationStackScroller.onExpansionStopped(); 545 } 546 547 @Override 548 public void onHeightChanged(ExpandableView view) { 549 requestPanelHeightUpdate(); 550 } 551 552 @Override 553 public void onScrollChanged() { 554 if (mQsExpanded) { 555 mNotificationStackScroller.setTranslationY( 556 mNotificationTranslation - mScrollView.getScrollY()); 557 } 558 } 559 560 @Override 561 public void onClick(View v) { 562 if (v == mHeader.getBackgroundView()) { 563 onQsExpansionStarted(); 564 if (mQsExpanded) { 565 flingSettings(0 /* vel */, false /* expand */); 566 } else if (mQsExpansionEnabled) { 567 flingSettings(0 /* vel */, true /* expand */); 568 } 569 } 570 } 571} 572