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