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