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