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