NotificationPanelView.java revision 12a6782d61dbefd5ba0bb286372936f957849637
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.ObjectAnimator; 22import android.animation.PropertyValuesHolder; 23import android.animation.ValueAnimator; 24import android.content.Context; 25import android.content.res.Configuration; 26import android.util.AttributeSet; 27import android.util.MathUtils; 28import android.view.MotionEvent; 29import android.view.VelocityTracker; 30import android.view.View; 31import android.view.ViewTreeObserver; 32import android.view.accessibility.AccessibilityEvent; 33import android.view.animation.AnimationUtils; 34import android.view.animation.Interpolator; 35import android.widget.FrameLayout; 36import android.widget.TextView; 37 38import com.android.keyguard.KeyguardStatusView; 39import com.android.systemui.R; 40import com.android.systemui.qs.QSPanel; 41import com.android.systemui.statusbar.ExpandableView; 42import com.android.systemui.statusbar.FlingAnimationUtils; 43import com.android.systemui.statusbar.GestureRecorder; 44import com.android.systemui.statusbar.KeyguardAffordanceView; 45import com.android.systemui.statusbar.StatusBarState; 46import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; 47import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 48import com.android.systemui.statusbar.stack.StackStateAnimator; 49 50public class NotificationPanelView extends PanelView implements 51 ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, 52 View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, 53 KeyguardAffordanceHelper.Callback { 54 55 // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is 56 // changed. 57 private static final int CAP_HEIGHT = 1456; 58 private static final int FONT_HEIGHT = 2163; 59 60 private static final float HEADER_RUBBERBAND_FACTOR = 2.05f; 61 private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f; 62 63 private KeyguardAffordanceHelper mAfforanceHelper; 64 private StatusBarHeaderView mHeader; 65 private KeyguardUserSwitcher mKeyguardUserSwitcher; 66 private KeyguardStatusBarView mKeyguardStatusBar; 67 private View mQsContainer; 68 private QSPanel mQsPanel; 69 private KeyguardStatusView mKeyguardStatusView; 70 private ObservableScrollView mScrollView; 71 private TextView mClockView; 72 private View mReserveNotificationSpace; 73 private View mQsNavbarScrim; 74 private View mNotificationContainerParent; 75 private NotificationStackScrollLayout mNotificationStackScroller; 76 private int mNotificationTopPadding; 77 private boolean mAnimateNextTopPaddingChange; 78 79 private int mTrackingPointer; 80 private VelocityTracker mVelocityTracker; 81 private boolean mQsTracking; 82 83 /** 84 * Handles launching the secure camera properly even when other applications may be using the 85 * camera hardware. 86 */ 87 private SecureCameraLaunchManager mSecureCameraLaunchManager; 88 89 /** 90 * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and 91 * the expansion for quick settings. 92 */ 93 private boolean mConflictingQsExpansionGesture; 94 95 /** 96 * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't 97 * intercepted yet. 98 */ 99 private boolean mIntercepting; 100 private boolean mQsExpanded; 101 private boolean mQsExpandedWhenExpandingStarted; 102 private boolean mQsFullyExpanded; 103 private boolean mKeyguardShowing; 104 private boolean mDozing; 105 private int mStatusBarState; 106 private float mInitialHeightOnTouch; 107 private float mInitialTouchX; 108 private float mInitialTouchY; 109 private float mLastTouchX; 110 private float mLastTouchY; 111 private float mQsExpansionHeight; 112 private int mQsMinExpansionHeight; 113 private int mQsMaxExpansionHeight; 114 private int mQsPeekHeight; 115 private boolean mStackScrollerOverscrolling; 116 private boolean mQsExpansionFromOverscroll; 117 private float mLastOverscroll; 118 private boolean mQsExpansionEnabled = true; 119 private ValueAnimator mQsExpansionAnimator; 120 private FlingAnimationUtils mFlingAnimationUtils; 121 private int mStatusBarMinHeight; 122 private boolean mUnlockIconActive; 123 private int mNotificationsHeaderCollideDistance; 124 private int mUnlockMoveDistance; 125 private float mEmptyDragAmount; 126 127 private Interpolator mFastOutSlowInInterpolator; 128 private Interpolator mFastOutLinearInterpolator; 129 private ObjectAnimator mClockAnimator; 130 private int mClockAnimationTarget = -1; 131 private int mTopPaddingAdjustment; 132 private KeyguardClockPositionAlgorithm mClockPositionAlgorithm = 133 new KeyguardClockPositionAlgorithm(); 134 private KeyguardClockPositionAlgorithm.Result mClockPositionResult = 135 new KeyguardClockPositionAlgorithm.Result(); 136 private boolean mIsExpanding; 137 138 private boolean mBlockTouches; 139 private int mNotificationScrimWaitDistance; 140 private boolean mTwoFingerQsExpand; 141 private boolean mTwoFingerQsExpandPossible; 142 143 /** 144 * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still 145 * need to take this into account in our panel height calculation. 146 */ 147 private int mScrollYOverride = -1; 148 private boolean mQsAnimatorExpand; 149 private boolean mIsLaunchTransitionFinished; 150 private boolean mIsLaunchTransitionRunning; 151 private Runnable mLaunchAnimationEndRunnable; 152 private boolean mOnlyAffordanceInThisMotion; 153 private boolean mKeyguardStatusViewAnimating; 154 private boolean mHeaderAnimatingIn; 155 private ObjectAnimator mQsContainerAnimator; 156 157 private boolean mShadeEmpty; 158 159 private boolean mQsScrimEnabled = true; 160 private boolean mLastAnnouncementWasQuickSettings; 161 private boolean mQsTouchAboveFalsingThreshold; 162 private int mQsFalsingThreshold; 163 164 public NotificationPanelView(Context context, AttributeSet attrs) { 165 super(context, attrs); 166 } 167 168 public void setStatusBar(PhoneStatusBar bar) { 169 mStatusBar = bar; 170 } 171 172 @Override 173 protected void onFinishInflate() { 174 super.onFinishInflate(); 175 mHeader = (StatusBarHeaderView) findViewById(R.id.header); 176 mHeader.setOnClickListener(this); 177 mKeyguardStatusBar = (KeyguardStatusBarView) findViewById(R.id.keyguard_header); 178 mKeyguardStatusView = (KeyguardStatusView) findViewById(R.id.keyguard_status_view); 179 mQsContainer = findViewById(R.id.quick_settings_container); 180 mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel); 181 mClockView = (TextView) findViewById(R.id.clock_view); 182 mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view); 183 mScrollView.setListener(this); 184 mScrollView.setFocusable(false); 185 mReserveNotificationSpace = findViewById(R.id.reserve_notification_space); 186 mNotificationContainerParent = findViewById(R.id.notification_container_parent); 187 mNotificationStackScroller = (NotificationStackScrollLayout) 188 findViewById(R.id.notification_stack_scroller); 189 mNotificationStackScroller.setOnHeightChangedListener(this); 190 mNotificationStackScroller.setOverscrollTopChangedListener(this); 191 mNotificationStackScroller.setScrollView(mScrollView); 192 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), 193 android.R.interpolator.fast_out_slow_in); 194 mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(), 195 android.R.interpolator.fast_out_linear_in); 196 mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area); 197 mQsNavbarScrim = findViewById(R.id.qs_navbar_scrim); 198 mAfforanceHelper = new KeyguardAffordanceHelper(this, getContext()); 199 mSecureCameraLaunchManager = 200 new SecureCameraLaunchManager(getContext(), mKeyguardBottomArea); 201 202 // recompute internal state when qspanel height changes 203 mQsContainer.addOnLayoutChangeListener(new OnLayoutChangeListener() { 204 @Override 205 public void onLayoutChange(View v, int left, int top, int right, 206 int bottom, int oldLeft, int oldTop, int oldRight, 207 int oldBottom) { 208 final int height = bottom - top; 209 final int oldHeight = oldBottom - oldTop; 210 if (height != oldHeight) { 211 onScrollChanged(); 212 } 213 } 214 }); 215 } 216 217 @Override 218 protected void loadDimens() { 219 super.loadDimens(); 220 mNotificationTopPadding = getResources().getDimensionPixelSize( 221 R.dimen.notifications_top_padding); 222 mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f); 223 mStatusBarMinHeight = getResources().getDimensionPixelSize( 224 com.android.internal.R.dimen.status_bar_height); 225 mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height); 226 mNotificationsHeaderCollideDistance = 227 getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance); 228 mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance); 229 mClockPositionAlgorithm.loadDimens(getResources()); 230 mNotificationScrimWaitDistance = 231 getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance); 232 mQsFalsingThreshold = getResources().getDimensionPixelSize( 233 R.dimen.qs_falsing_threshold); 234 } 235 236 public void updateResources() { 237 int panelWidth = getResources().getDimensionPixelSize(R.dimen.notification_panel_width); 238 int panelGravity = getResources().getInteger(R.integer.notification_panel_layout_gravity); 239 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mHeader.getLayoutParams(); 240 if (lp.width != panelWidth) { 241 lp.width = panelWidth; 242 lp.gravity = panelGravity; 243 mHeader.setLayoutParams(lp); 244 mHeader.post(mUpdateHeader); 245 } 246 247 lp = (FrameLayout.LayoutParams) mNotificationStackScroller.getLayoutParams(); 248 if (lp.width != panelWidth) { 249 lp.width = panelWidth; 250 lp.gravity = panelGravity; 251 mNotificationStackScroller.setLayoutParams(lp); 252 } 253 254 lp = (FrameLayout.LayoutParams) mScrollView.getLayoutParams(); 255 if (lp.width != panelWidth) { 256 lp.width = panelWidth; 257 lp.gravity = panelGravity; 258 mScrollView.setLayoutParams(lp); 259 } 260 } 261 262 @Override 263 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 264 super.onLayout(changed, left, top, right, bottom); 265 266 // Update Clock Pivot 267 mKeyguardStatusView.setPivotX(getWidth() / 2); 268 mKeyguardStatusView.setPivotY((FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize()); 269 270 // Calculate quick setting heights. 271 mQsMinExpansionHeight = mKeyguardShowing ? 0 : mHeader.getCollapsedHeight() + mQsPeekHeight; 272 mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight(); 273 if (mQsExpanded) { 274 if (mQsFullyExpanded) { 275 mQsExpansionHeight = mQsMaxExpansionHeight; 276 requestScrollerTopPaddingUpdate(false /* animate */); 277 } 278 } else { 279 setQsExpansion(mQsMinExpansionHeight + mLastOverscroll); 280 positionClockAndNotifications(); 281 mNotificationStackScroller.setStackHeight(getExpandedHeight()); 282 updateHeader(); 283 } 284 mNotificationStackScroller.updateIsSmallScreen( 285 mHeader.getCollapsedHeight() + mQsPeekHeight); 286 } 287 288 @Override 289 public void onAttachedToWindow() { 290 mSecureCameraLaunchManager.create(); 291 } 292 293 @Override 294 public void onDetachedFromWindow() { 295 mSecureCameraLaunchManager.destroy(); 296 } 297 298 /** 299 * Positions the clock and notifications dynamically depending on how many notifications are 300 * showing. 301 */ 302 private void positionClockAndNotifications() { 303 boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending(); 304 int stackScrollerPadding; 305 if (mStatusBarState != StatusBarState.KEYGUARD) { 306 int bottom = mHeader.getCollapsedHeight(); 307 stackScrollerPadding = mStatusBarState == StatusBarState.SHADE 308 ? bottom + mQsPeekHeight + mNotificationTopPadding 309 : mKeyguardStatusBar.getHeight() + mNotificationTopPadding; 310 mTopPaddingAdjustment = 0; 311 } else { 312 mClockPositionAlgorithm.setup( 313 mStatusBar.getMaxKeyguardNotifications(), 314 getMaxPanelHeight(), 315 getExpandedHeight(), 316 mNotificationStackScroller.getNotGoneChildCount(), 317 getHeight(), 318 mKeyguardStatusView.getHeight(), 319 mEmptyDragAmount); 320 mClockPositionAlgorithm.run(mClockPositionResult); 321 if (animate || mClockAnimator != null) { 322 startClockAnimation(mClockPositionResult.clockY); 323 } else { 324 mKeyguardStatusView.setY(mClockPositionResult.clockY); 325 } 326 updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale); 327 stackScrollerPadding = mClockPositionResult.stackScrollerPadding; 328 mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment; 329 } 330 mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding); 331 requestScrollerTopPaddingUpdate(animate); 332 } 333 334 private void startClockAnimation(int y) { 335 if (mClockAnimationTarget == y) { 336 return; 337 } 338 mClockAnimationTarget = y; 339 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 340 @Override 341 public boolean onPreDraw() { 342 getViewTreeObserver().removeOnPreDrawListener(this); 343 if (mClockAnimator != null) { 344 mClockAnimator.removeAllListeners(); 345 mClockAnimator.cancel(); 346 } 347 mClockAnimator = ObjectAnimator 348 .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget); 349 mClockAnimator.setInterpolator(mFastOutSlowInInterpolator); 350 mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 351 mClockAnimator.addListener(new AnimatorListenerAdapter() { 352 @Override 353 public void onAnimationEnd(Animator animation) { 354 mClockAnimator = null; 355 mClockAnimationTarget = -1; 356 } 357 }); 358 mClockAnimator.start(); 359 return true; 360 } 361 }); 362 } 363 364 private void updateClock(float alpha, float scale) { 365 if (!mKeyguardStatusViewAnimating) { 366 mKeyguardStatusView.setAlpha(alpha); 367 } 368 mKeyguardStatusView.setScaleX(scale); 369 mKeyguardStatusView.setScaleY(scale); 370 } 371 372 public void animateToFullShade(long delay) { 373 mAnimateNextTopPaddingChange = true; 374 mNotificationStackScroller.goToFullShade(delay); 375 requestLayout(); 376 } 377 378 public void setQsExpansionEnabled(boolean qsExpansionEnabled) { 379 mQsExpansionEnabled = qsExpansionEnabled; 380 mHeader.setClickable(qsExpansionEnabled); 381 } 382 383 @Override 384 public void resetViews() { 385 mIsLaunchTransitionFinished = false; 386 mBlockTouches = false; 387 mUnlockIconActive = false; 388 mAfforanceHelper.reset(true); 389 closeQs(); 390 mStatusBar.dismissPopups(); 391 mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */, 392 true /* cancelAnimators */); 393 } 394 395 public void closeQs() { 396 cancelAnimation(); 397 setQsExpansion(mQsMinExpansionHeight); 398 } 399 400 public void animateCloseQs() { 401 if (mQsExpansionAnimator != null) { 402 if (!mQsAnimatorExpand) { 403 return; 404 } 405 float height = mQsExpansionHeight; 406 mQsExpansionAnimator.cancel(); 407 setQsExpansion(height); 408 } 409 flingSettings(0 /* vel */, false); 410 } 411 412 public void openQs() { 413 cancelAnimation(); 414 if (mQsExpansionEnabled) { 415 setQsExpansion(mQsMaxExpansionHeight); 416 } 417 } 418 419 @Override 420 public void fling(float vel, boolean expand) { 421 GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); 422 if (gr != null) { 423 gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); 424 } 425 super.fling(vel, expand); 426 } 427 428 @Override 429 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 430 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 431 event.getText().add(getKeyguardOrLockScreenString()); 432 mLastAnnouncementWasQuickSettings = false; 433 return true; 434 } 435 436 return super.dispatchPopulateAccessibilityEvent(event); 437 } 438 439 @Override 440 public boolean onInterceptTouchEvent(MotionEvent event) { 441 if (mBlockTouches) { 442 return false; 443 } 444 resetDownStates(event); 445 int pointerIndex = event.findPointerIndex(mTrackingPointer); 446 if (pointerIndex < 0) { 447 pointerIndex = 0; 448 mTrackingPointer = event.getPointerId(pointerIndex); 449 } 450 final float x = event.getX(pointerIndex); 451 final float y = event.getY(pointerIndex); 452 453 switch (event.getActionMasked()) { 454 case MotionEvent.ACTION_DOWN: 455 mIntercepting = true; 456 mInitialTouchY = y; 457 mInitialTouchX = x; 458 initVelocityTracker(); 459 trackMovement(event); 460 if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { 461 getParent().requestDisallowInterceptTouchEvent(true); 462 } 463 if (mQsExpansionAnimator != null) { 464 onQsExpansionStarted(); 465 mInitialHeightOnTouch = mQsExpansionHeight; 466 mQsTracking = true; 467 mIntercepting = false; 468 mNotificationStackScroller.removeLongPressCallback(); 469 } 470 break; 471 case MotionEvent.ACTION_POINTER_UP: 472 final int upPointer = event.getPointerId(event.getActionIndex()); 473 if (mTrackingPointer == upPointer) { 474 // gesture is ongoing, find a new pointer to track 475 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 476 mTrackingPointer = event.getPointerId(newIndex); 477 mInitialTouchX = event.getX(newIndex); 478 mInitialTouchY = event.getY(newIndex); 479 } 480 break; 481 482 case MotionEvent.ACTION_MOVE: 483 final float h = y - mInitialTouchY; 484 trackMovement(event); 485 if (mQsTracking) { 486 487 // Already tracking because onOverscrolled was called. We need to update here 488 // so we don't stop for a frame until the next touch event gets handled in 489 // onTouchEvent. 490 setQsExpansion(h + mInitialHeightOnTouch); 491 trackMovement(event); 492 mIntercepting = false; 493 return true; 494 } 495 if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX) 496 && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { 497 onQsExpansionStarted(); 498 mInitialHeightOnTouch = mQsExpansionHeight; 499 mInitialTouchY = y; 500 mInitialTouchX = x; 501 mQsTracking = true; 502 mIntercepting = false; 503 mNotificationStackScroller.removeLongPressCallback(); 504 return true; 505 } 506 break; 507 508 case MotionEvent.ACTION_CANCEL: 509 case MotionEvent.ACTION_UP: 510 trackMovement(event); 511 if (mQsTracking) { 512 flingQsWithCurrentVelocity(); 513 mQsTracking = false; 514 } 515 mIntercepting = false; 516 break; 517 } 518 519 // Allow closing the whole panel when in SHADE state. 520 if (mStatusBarState == StatusBarState.SHADE) { 521 return super.onInterceptTouchEvent(event); 522 } else { 523 return !mQsExpanded && super.onInterceptTouchEvent(event); 524 } 525 } 526 527 private void resetDownStates(MotionEvent event) { 528 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 529 mOnlyAffordanceInThisMotion = false; 530 mQsTouchAboveFalsingThreshold = mQsFullyExpanded; 531 } 532 } 533 534 @Override 535 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 536 537 // Block request when interacting with the scroll view so we can still intercept the 538 // scrolling when QS is expanded. 539 if (mScrollView.isHandlingTouchEvent()) { 540 return; 541 } 542 super.requestDisallowInterceptTouchEvent(disallowIntercept); 543 } 544 545 private void flingQsWithCurrentVelocity() { 546 float vel = getCurrentVelocity(); 547 flingSettings(vel, flingExpandsQs(vel)); 548 } 549 550 private boolean flingExpandsQs(float vel) { 551 if (isBelowFalsingThreshold()) { 552 return false; 553 } 554 if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 555 return getQsExpansionFraction() > 0.5f; 556 } else { 557 return vel > 0; 558 } 559 } 560 561 private boolean isBelowFalsingThreshold() { 562 return !mQsTouchAboveFalsingThreshold && mStatusBar.isFalsingThresholdNeeded(); 563 } 564 565 private float getQsExpansionFraction() { 566 return Math.min(1f, (mQsExpansionHeight - mQsMinExpansionHeight) 567 / (getTempQsMaxExpansion() - mQsMinExpansionHeight)); 568 } 569 570 @Override 571 public boolean onTouchEvent(MotionEvent event) { 572 if (mBlockTouches) { 573 return false; 574 } 575 resetDownStates(event); 576 if ((!mIsExpanding || mHintAnimationRunning) 577 && !mQsExpanded 578 && mStatusBar.getBarState() != StatusBarState.SHADE) { 579 mAfforanceHelper.onTouchEvent(event); 580 } 581 if (mOnlyAffordanceInThisMotion) { 582 return true; 583 } 584 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f 585 && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded 586 && mQsExpansionEnabled) { 587 588 // Down in the empty area while fully expanded - go to QS. 589 mQsTracking = true; 590 mConflictingQsExpansionGesture = true; 591 onQsExpansionStarted(); 592 mInitialHeightOnTouch = mQsExpansionHeight; 593 mInitialTouchY = event.getX(); 594 mInitialTouchX = event.getY(); 595 } 596 if (mExpandedHeight != 0) { 597 handleQsDown(event); 598 } 599 if (!mTwoFingerQsExpand && mQsTracking) { 600 onQsTouch(event); 601 if (!mConflictingQsExpansionGesture) { 602 return true; 603 } 604 } 605 if (event.getActionMasked() == MotionEvent.ACTION_CANCEL 606 || event.getActionMasked() == MotionEvent.ACTION_UP) { 607 mConflictingQsExpansionGesture = false; 608 } 609 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0 610 && mQsExpansionEnabled) { 611 mTwoFingerQsExpandPossible = true; 612 } 613 if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN 614 && event.getPointerCount() == 2 615 && event.getY(event.getActionIndex()) < mStatusBarMinHeight) { 616 mTwoFingerQsExpand = true; 617 requestPanelHeightUpdate(); 618 619 // Normally, we start listening when the panel is expanded, but here we need to start 620 // earlier so the state is already up to date when dragging down. 621 setListening(true); 622 } 623 super.onTouchEvent(event); 624 return true; 625 } 626 627 private boolean isInQsArea(float x, float y) { 628 return mStatusBarState != StatusBarState.SHADE 629 || y <= mNotificationStackScroller.getBottomMostNotificationBottom() 630 || y <= mQsContainer.getY() + mQsContainer.getHeight(); 631 } 632 633 private void handleQsDown(MotionEvent event) { 634 if (event.getActionMasked() == MotionEvent.ACTION_DOWN 635 && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) { 636 mQsTracking = true; 637 onQsExpansionStarted(); 638 mInitialHeightOnTouch = mQsExpansionHeight; 639 mInitialTouchY = event.getX(); 640 mInitialTouchX = event.getY(); 641 642 // If we interrupt an expansion gesture here, make sure to update the state correctly. 643 if (mIsExpanding) { 644 onExpandingFinished(); 645 } 646 } 647 } 648 649 @Override 650 protected boolean flingExpands(float vel, float vectorVel) { 651 boolean expands = super.flingExpands(vel, vectorVel); 652 653 // If we are already running a QS expansion, make sure that we keep the panel open. 654 if (mQsExpansionAnimator != null) { 655 expands = true; 656 } 657 return expands; 658 } 659 660 @Override 661 protected boolean hasConflictingGestures() { 662 return mStatusBar.getBarState() != StatusBarState.SHADE; 663 } 664 665 private void onQsTouch(MotionEvent event) { 666 int pointerIndex = event.findPointerIndex(mTrackingPointer); 667 if (pointerIndex < 0) { 668 pointerIndex = 0; 669 mTrackingPointer = event.getPointerId(pointerIndex); 670 } 671 final float y = event.getY(pointerIndex); 672 final float x = event.getX(pointerIndex); 673 674 switch (event.getActionMasked()) { 675 case MotionEvent.ACTION_DOWN: 676 mQsTracking = true; 677 mInitialTouchY = y; 678 mInitialTouchX = x; 679 onQsExpansionStarted(); 680 mInitialHeightOnTouch = mQsExpansionHeight; 681 initVelocityTracker(); 682 trackMovement(event); 683 break; 684 685 case MotionEvent.ACTION_POINTER_UP: 686 final int upPointer = event.getPointerId(event.getActionIndex()); 687 if (mTrackingPointer == upPointer) { 688 // gesture is ongoing, find a new pointer to track 689 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 690 final float newY = event.getY(newIndex); 691 final float newX = event.getX(newIndex); 692 mTrackingPointer = event.getPointerId(newIndex); 693 mInitialHeightOnTouch = mQsExpansionHeight; 694 mInitialTouchY = newY; 695 mInitialTouchX = newX; 696 } 697 break; 698 699 case MotionEvent.ACTION_MOVE: 700 final float h = y - mInitialTouchY; 701 setQsExpansion(h + mInitialHeightOnTouch); 702 if (h >= mQsFalsingThreshold) { 703 mQsTouchAboveFalsingThreshold = true; 704 } 705 trackMovement(event); 706 break; 707 708 case MotionEvent.ACTION_UP: 709 case MotionEvent.ACTION_CANCEL: 710 mQsTracking = false; 711 mTrackingPointer = -1; 712 trackMovement(event); 713 float fraction = getQsExpansionFraction(); 714 if ((fraction != 0f || y >= mInitialTouchY) 715 && (fraction != 1f || y <= mInitialTouchY)) { 716 flingQsWithCurrentVelocity(); 717 } else { 718 mScrollYOverride = -1; 719 } 720 if (mVelocityTracker != null) { 721 mVelocityTracker.recycle(); 722 mVelocityTracker = null; 723 } 724 break; 725 } 726 } 727 728 @Override 729 public void onOverscrolled(float lastTouchX, float lastTouchY, int amount) { 730 if (mIntercepting && shouldQuickSettingsIntercept(lastTouchX, lastTouchY, 731 -1 /* yDiff: Not relevant here */)) { 732 onQsExpansionStarted(amount); 733 mInitialHeightOnTouch = mQsExpansionHeight; 734 mInitialTouchY = mLastTouchY; 735 mInitialTouchX = mLastTouchX; 736 mQsTracking = true; 737 } 738 } 739 740 @Override 741 public void onOverscrollTopChanged(float amount, boolean isRubberbanded) { 742 cancelAnimation(); 743 if (!mQsExpansionEnabled) { 744 amount = 0f; 745 } 746 float rounded = amount >= 1f ? amount : 0f; 747 mStackScrollerOverscrolling = rounded != 0f && isRubberbanded; 748 mQsExpansionFromOverscroll = rounded != 0f; 749 mLastOverscroll = rounded; 750 updateQsState(); 751 setQsExpansion(mQsMinExpansionHeight + rounded); 752 } 753 754 @Override 755 public void flingTopOverscroll(float velocity, boolean open) { 756 mLastOverscroll = 0f; 757 setQsExpansion(mQsExpansionHeight); 758 flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled, 759 new Runnable() { 760 @Override 761 public void run() { 762 mStackScrollerOverscrolling = false; 763 mQsExpansionFromOverscroll = false; 764 updateQsState(); 765 } 766 }); 767 } 768 769 private void onQsExpansionStarted() { 770 onQsExpansionStarted(0); 771 } 772 773 private void onQsExpansionStarted(int overscrollAmount) { 774 cancelAnimation(); 775 776 // Reset scroll position and apply that position to the expanded height. 777 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 778 if (mScrollView.getScrollY() != 0) { 779 mScrollYOverride = mScrollView.getScrollY(); 780 } 781 mScrollView.scrollTo(0, 0); 782 setQsExpansion(height); 783 } 784 785 private void setQsExpanded(boolean expanded) { 786 boolean changed = mQsExpanded != expanded; 787 if (changed) { 788 mQsExpanded = expanded; 789 updateQsState(); 790 requestPanelHeightUpdate(); 791 mNotificationStackScroller.setInterceptDelegateEnabled(expanded); 792 mStatusBar.setQsExpanded(expanded); 793 } 794 } 795 796 public void setBarState(int statusBarState, boolean keyguardFadingAway, 797 boolean goingToFullShade) { 798 boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD 799 || statusBarState == StatusBarState.SHADE_LOCKED; 800 if (!mKeyguardShowing && keyguardShowing) { 801 setQsTranslation(mQsExpansionHeight); 802 mHeader.setTranslationY(0f); 803 } 804 setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade); 805 setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); 806 if (goingToFullShade) { 807 animateKeyguardStatusBarOut(); 808 } else { 809 mKeyguardStatusBar.setAlpha(1f); 810 mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE); 811 } 812 mStatusBarState = statusBarState; 813 mKeyguardShowing = keyguardShowing; 814 updateQsState(); 815 if (goingToFullShade) { 816 animateHeaderSlidingIn(); 817 } 818 } 819 820 private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() { 821 @Override 822 public void run() { 823 mKeyguardStatusViewAnimating = false; 824 mKeyguardStatusView.setVisibility(View.GONE); 825 } 826 }; 827 828 private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() { 829 @Override 830 public void run() { 831 mKeyguardStatusViewAnimating = false; 832 } 833 }; 834 835 private final Animator.AnimatorListener mAnimateHeaderSlidingInListener 836 = new AnimatorListenerAdapter() { 837 @Override 838 public void onAnimationEnd(Animator animation) { 839 mHeaderAnimatingIn = false; 840 mQsContainerAnimator = null; 841 mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater); 842 } 843 }; 844 845 private final OnLayoutChangeListener mQsContainerAnimatorUpdater 846 = new OnLayoutChangeListener() { 847 @Override 848 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 849 int oldTop, int oldRight, int oldBottom) { 850 int oldHeight = oldBottom - oldTop; 851 int height = bottom - top; 852 if (height != oldHeight && mQsContainerAnimator != null) { 853 PropertyValuesHolder[] values = mQsContainerAnimator.getValues(); 854 float newEndValue = mHeader.getCollapsedHeight() + mQsPeekHeight - height - top; 855 float newStartValue = -height - top; 856 values[0].setFloatValues(newStartValue, newEndValue); 857 mQsContainerAnimator.setCurrentPlayTime(mQsContainerAnimator.getCurrentPlayTime()); 858 } 859 } 860 }; 861 862 private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn 863 = new ViewTreeObserver.OnPreDrawListener() { 864 @Override 865 public boolean onPreDraw() { 866 getViewTreeObserver().removeOnPreDrawListener(this); 867 mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight); 868 mHeader.animate() 869 .translationY(0f) 870 .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()) 871 .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) 872 .setInterpolator(mFastOutSlowInInterpolator) 873 .start(); 874 mQsContainer.setY(-mQsContainer.getHeight()); 875 mQsContainerAnimator = ObjectAnimator.ofFloat(mQsContainer, View.TRANSLATION_Y, 876 mQsContainer.getTranslationY(), 877 mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight() 878 - mQsContainer.getTop()); 879 mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()); 880 mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE); 881 mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator); 882 mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener); 883 mQsContainerAnimator.start(); 884 mQsContainer.addOnLayoutChangeListener(mQsContainerAnimatorUpdater); 885 return true; 886 } 887 }; 888 889 private void animateHeaderSlidingIn() { 890 mHeaderAnimatingIn = true; 891 getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); 892 893 } 894 895 private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() { 896 @Override 897 public void run() { 898 mKeyguardStatusBar.setVisibility(View.INVISIBLE); 899 } 900 }; 901 902 private void animateKeyguardStatusBarOut() { 903 mKeyguardStatusBar.animate() 904 .alpha(0f) 905 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 906 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 907 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 908 .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable) 909 .start(); 910 } 911 912 private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() { 913 @Override 914 public void run() { 915 mKeyguardBottomArea.setVisibility(View.GONE); 916 } 917 }; 918 919 private void setKeyguardBottomAreaVisibility(int statusBarState, 920 boolean goingToFullShade) { 921 if (goingToFullShade) { 922 mKeyguardBottomArea.animate().cancel(); 923 mKeyguardBottomArea.animate() 924 .alpha(0f) 925 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 926 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 927 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 928 .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable) 929 .start(); 930 } else if (statusBarState == StatusBarState.KEYGUARD 931 || statusBarState == StatusBarState.SHADE_LOCKED) { 932 mKeyguardBottomArea.animate().cancel(); 933 mKeyguardBottomArea.setVisibility(View.VISIBLE); 934 mKeyguardBottomArea.setAlpha(1f); 935 } else { 936 mKeyguardBottomArea.animate().cancel(); 937 mKeyguardBottomArea.setVisibility(View.GONE); 938 mKeyguardBottomArea.setAlpha(1f); 939 } 940 } 941 942 private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway, 943 boolean goingToFullShade) { 944 if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD 945 && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) { 946 mKeyguardStatusView.animate().cancel(); 947 mKeyguardStatusViewAnimating = true; 948 mKeyguardStatusView.animate() 949 .alpha(0f) 950 .setStartDelay(0) 951 .setDuration(160) 952 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 953 .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable); 954 if (keyguardFadingAway) { 955 mKeyguardStatusView.animate() 956 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 957 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 958 .start(); 959 } 960 } else if (mStatusBarState == StatusBarState.SHADE_LOCKED 961 && statusBarState == StatusBarState.KEYGUARD) { 962 mKeyguardStatusView.animate().cancel(); 963 mKeyguardStatusView.setVisibility(View.VISIBLE); 964 mKeyguardStatusViewAnimating = true; 965 mKeyguardStatusView.setAlpha(0f); 966 mKeyguardStatusView.animate() 967 .alpha(1f) 968 .setStartDelay(0) 969 .setDuration(320) 970 .setInterpolator(PhoneStatusBar.ALPHA_IN) 971 .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable); 972 } else if (statusBarState == StatusBarState.KEYGUARD) { 973 mKeyguardStatusView.animate().cancel(); 974 mKeyguardStatusView.setVisibility(View.VISIBLE); 975 mKeyguardStatusView.setAlpha(1f); 976 } else { 977 mKeyguardStatusView.animate().cancel(); 978 mKeyguardStatusView.setVisibility(View.GONE); 979 mKeyguardStatusView.setAlpha(1f); 980 } 981 } 982 983 private void updateQsState() { 984 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; 985 mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE); 986 mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling)); 987 mNotificationStackScroller.setScrollingEnabled( 988 mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded 989 || mQsExpansionFromOverscroll)); 990 mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 991 mQsContainer.setVisibility( 992 mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE); 993 mScrollView.setTouchEnabled(mQsExpanded); 994 updateEmptyShadeView(); 995 mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded 996 && !mStackScrollerOverscrolling && mQsScrimEnabled 997 ? View.VISIBLE 998 : View.INVISIBLE); 999 if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) { 1000 mKeyguardUserSwitcher.hide(true /* animate */); 1001 } 1002 } 1003 1004 private void setQsExpansion(float height) { 1005 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); 1006 mQsFullyExpanded = height == mQsMaxExpansionHeight; 1007 if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) { 1008 setQsExpanded(true); 1009 } else if (height <= mQsMinExpansionHeight && mQsExpanded) { 1010 setQsExpanded(false); 1011 if (mLastAnnouncementWasQuickSettings && !mTracking) { 1012 announceForAccessibility(getKeyguardOrLockScreenString()); 1013 mLastAnnouncementWasQuickSettings = false; 1014 } 1015 } 1016 mQsExpansionHeight = height; 1017 mHeader.setExpansion(getHeaderExpansionFraction()); 1018 setQsTranslation(height); 1019 requestScrollerTopPaddingUpdate(false /* animate */); 1020 updateNotificationScrim(height); 1021 if (mKeyguardShowing) { 1022 updateHeaderKeyguard(); 1023 } 1024 if (mStatusBarState == StatusBarState.SHADE && mQsExpanded 1025 && !mStackScrollerOverscrolling && mQsScrimEnabled) { 1026 mQsNavbarScrim.setAlpha(getQsExpansionFraction()); 1027 } 1028 1029 // Upon initialisation when we are not layouted yet we don't want to announce that we are 1030 // fully expanded, hence the != 0.0f check. 1031 if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) { 1032 announceForAccessibility(getContext().getString( 1033 R.string.accessibility_desc_quick_settings)); 1034 mLastAnnouncementWasQuickSettings = true; 1035 } 1036 } 1037 1038 private String getKeyguardOrLockScreenString() { 1039 if (mStatusBarState == StatusBarState.KEYGUARD) { 1040 return getContext().getString(R.string.accessibility_desc_lock_screen); 1041 } else { 1042 return getContext().getString(R.string.accessibility_desc_notification_shade); 1043 } 1044 } 1045 1046 private void updateNotificationScrim(float height) { 1047 int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance; 1048 float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance); 1049 progress = Math.max(0.0f, Math.min(progress, 1.0f)); 1050 } 1051 1052 private float getHeaderExpansionFraction() { 1053 if (!mKeyguardShowing) { 1054 return getQsExpansionFraction(); 1055 } else { 1056 return 1f; 1057 } 1058 } 1059 1060 private void setQsTranslation(float height) { 1061 if (!mHeaderAnimatingIn) { 1062 mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation()); 1063 } 1064 if (mKeyguardShowing) { 1065 mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0)); 1066 } 1067 } 1068 1069 private float calculateQsTopPadding() { 1070 // We can only do the smoother transition on Keyguard when we also are not collapsing from a 1071 // scrolled quick settings. 1072 if (mKeyguardShowing && mScrollYOverride == -1) { 1073 return interpolate(getQsExpansionFraction(), 1074 mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding, 1075 mQsMaxExpansionHeight); 1076 } else { 1077 return mQsExpansionHeight; 1078 } 1079 } 1080 1081 private void requestScrollerTopPaddingUpdate(boolean animate) { 1082 mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(), 1083 mScrollView.getScrollY(), 1084 mAnimateNextTopPaddingChange || animate); 1085 mAnimateNextTopPaddingChange = false; 1086 } 1087 1088 private void trackMovement(MotionEvent event) { 1089 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 1090 mLastTouchX = event.getX(); 1091 mLastTouchY = event.getY(); 1092 } 1093 1094 private void initVelocityTracker() { 1095 if (mVelocityTracker != null) { 1096 mVelocityTracker.recycle(); 1097 } 1098 mVelocityTracker = VelocityTracker.obtain(); 1099 } 1100 1101 private float getCurrentVelocity() { 1102 if (mVelocityTracker == null) { 1103 return 0; 1104 } 1105 mVelocityTracker.computeCurrentVelocity(1000); 1106 return mVelocityTracker.getYVelocity(); 1107 } 1108 1109 private void cancelAnimation() { 1110 if (mQsExpansionAnimator != null) { 1111 mQsExpansionAnimator.cancel(); 1112 } 1113 } 1114 1115 private void flingSettings(float vel, boolean expand) { 1116 flingSettings(vel, expand, null); 1117 } 1118 1119 private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) { 1120 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 1121 if (target == mQsExpansionHeight) { 1122 mScrollYOverride = -1; 1123 if (onFinishRunnable != null) { 1124 onFinishRunnable.run(); 1125 } 1126 return; 1127 } 1128 boolean belowFalsingThreshold = isBelowFalsingThreshold(); 1129 if (belowFalsingThreshold) { 1130 vel = 0; 1131 } 1132 mScrollView.setBlockFlinging(true); 1133 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 1134 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 1135 if (belowFalsingThreshold) { 1136 animator.setDuration(350); 1137 } 1138 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1139 @Override 1140 public void onAnimationUpdate(ValueAnimator animation) { 1141 setQsExpansion((Float) animation.getAnimatedValue()); 1142 } 1143 }); 1144 animator.addListener(new AnimatorListenerAdapter() { 1145 @Override 1146 public void onAnimationEnd(Animator animation) { 1147 mScrollView.setBlockFlinging(false); 1148 mScrollYOverride = -1; 1149 mQsExpansionAnimator = null; 1150 if (onFinishRunnable != null) { 1151 onFinishRunnable.run(); 1152 } 1153 } 1154 }); 1155 animator.start(); 1156 mQsExpansionAnimator = animator; 1157 mQsAnimatorExpand = expand; 1158 } 1159 1160 /** 1161 * @return Whether we should intercept a gesture to open Quick Settings. 1162 */ 1163 private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { 1164 if (!mQsExpansionEnabled) { 1165 return false; 1166 } 1167 View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader; 1168 boolean onHeader = x >= header.getLeft() && x <= header.getRight() 1169 && y >= header.getTop() && y <= header.getBottom(); 1170 if (mQsExpanded) { 1171 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y); 1172 } else { 1173 return onHeader; 1174 } 1175 } 1176 1177 @Override 1178 protected boolean isScrolledToBottom() { 1179 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 1180 return true; 1181 } 1182 if (!isInSettings()) { 1183 return mNotificationStackScroller.isScrolledToBottom(); 1184 } else { 1185 return mScrollView.isScrolledToBottom(); 1186 } 1187 } 1188 1189 @Override 1190 protected int getMaxPanelHeight() { 1191 int min = mStatusBarMinHeight; 1192 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD 1193 && mNotificationStackScroller.getNotGoneChildCount() == 0) { 1194 int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount()) 1195 * HEADER_RUBBERBAND_FACTOR); 1196 min = Math.max(min, minHeight); 1197 } 1198 int maxHeight; 1199 if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) { 1200 maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade()); 1201 } else { 1202 maxHeight = calculatePanelHeightShade(); 1203 } 1204 maxHeight = Math.max(maxHeight, min); 1205 return maxHeight; 1206 } 1207 1208 private boolean isInSettings() { 1209 return mQsExpanded; 1210 } 1211 1212 @Override 1213 protected void onHeightUpdated(float expandedHeight) { 1214 if (!mQsExpanded) { 1215 positionClockAndNotifications(); 1216 } 1217 if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null 1218 && !mQsExpansionFromOverscroll) { 1219 float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding() 1220 + mNotificationStackScroller.getMinStackHeight() 1221 + mNotificationStackScroller.getNotificationTopPadding(); 1222 float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); 1223 float t = (expandedHeight - panelHeightQsCollapsed) 1224 / (panelHeightQsExpanded - panelHeightQsCollapsed); 1225 1226 setQsExpansion(mQsMinExpansionHeight 1227 + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight)); 1228 } 1229 mNotificationStackScroller.setStackHeight(expandedHeight); 1230 updateHeader(); 1231 updateUnlockIcon(); 1232 updateNotificationTranslucency(); 1233 } 1234 1235 /** 1236 * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when 1237 * collapsing QS / the panel when QS was scrolled 1238 */ 1239 private int getTempQsMaxExpansion() { 1240 int qsTempMaxExpansion = mQsMaxExpansionHeight; 1241 if (mScrollYOverride != -1) { 1242 qsTempMaxExpansion -= mScrollYOverride; 1243 } 1244 return qsTempMaxExpansion; 1245 } 1246 1247 private int calculatePanelHeightShade() { 1248 int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin(); 1249 int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin 1250 - mTopPaddingAdjustment; 1251 maxHeight += mNotificationStackScroller.getTopPaddingOverflow(); 1252 return maxHeight; 1253 } 1254 1255 private int calculatePanelHeightQsExpanded() { 1256 float notificationHeight = mNotificationStackScroller.getHeight() 1257 - mNotificationStackScroller.getEmptyBottomMargin() 1258 - mNotificationStackScroller.getTopPadding(); 1259 float totalHeight = mQsMaxExpansionHeight + notificationHeight 1260 + mNotificationStackScroller.getNotificationTopPadding(); 1261 if (totalHeight > mNotificationStackScroller.getHeight()) { 1262 float fullyCollapsedHeight = mQsMaxExpansionHeight 1263 + mNotificationStackScroller.getMinStackHeight() 1264 + mNotificationStackScroller.getNotificationTopPadding() 1265 - getScrollViewScrollY(); 1266 totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight()); 1267 } 1268 return (int) totalHeight; 1269 } 1270 1271 private int getScrollViewScrollY() { 1272 if (mScrollYOverride != -1) { 1273 return mScrollYOverride; 1274 } else { 1275 return mScrollView.getScrollY(); 1276 } 1277 } 1278 private void updateNotificationTranslucency() { 1279 float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight()) 1280 / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize() 1281 - mNotificationStackScroller.getCollapseSecondCardPadding()); 1282 alpha = Math.max(0, Math.min(alpha, 1)); 1283 alpha = (float) Math.pow(alpha, 0.75); 1284 if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) { 1285 mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null); 1286 } else if (alpha == 1f 1287 && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) { 1288 mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null); 1289 } 1290 mNotificationStackScroller.setAlpha(alpha); 1291 } 1292 1293 @Override 1294 protected float getOverExpansionAmount() { 1295 return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */); 1296 } 1297 1298 @Override 1299 protected float getOverExpansionPixels() { 1300 return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */); 1301 } 1302 1303 private void updateUnlockIcon() { 1304 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1305 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1306 boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance; 1307 KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon(); 1308 if (active && !mUnlockIconActive && mTracking) { 1309 lockIcon.setImageAlpha(1.0f, true, 150, mFastOutLinearInterpolator, null); 1310 lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150, 1311 mFastOutLinearInterpolator); 1312 } else if (!active && mUnlockIconActive && mTracking) { 1313 lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true, 1314 150, mFastOutLinearInterpolator, null); 1315 lockIcon.setImageScale(1.0f, true, 150, 1316 mFastOutLinearInterpolator); 1317 } 1318 mUnlockIconActive = active; 1319 } 1320 } 1321 1322 /** 1323 * Hides the header when notifications are colliding with it. 1324 */ 1325 private void updateHeader() { 1326 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1327 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1328 updateHeaderKeyguard(); 1329 } else { 1330 updateHeaderShade(); 1331 } 1332 1333 } 1334 1335 private void updateHeaderShade() { 1336 if (!mHeaderAnimatingIn) { 1337 mHeader.setTranslationY(getHeaderTranslation()); 1338 } 1339 setQsTranslation(mQsExpansionHeight); 1340 } 1341 1342 private float getHeaderTranslation() { 1343 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1344 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1345 return 0; 1346 } 1347 if (mNotificationStackScroller.getNotGoneChildCount() == 0) { 1348 if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) { 1349 return 0; 1350 } else { 1351 return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight; 1352 } 1353 } 1354 return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR; 1355 } 1356 1357 private void updateHeaderKeyguard() { 1358 float alphaNotifications; 1359 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 1360 1361 // When on Keyguard, we hide the header as soon as the top card of the notification 1362 // stack scroller is close enough (collision distance) to the bottom of the header. 1363 alphaNotifications = getNotificationsTopY() 1364 / 1365 (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance); 1366 } else { 1367 1368 // In SHADE_LOCKED, the top card is already really close to the header. Hide it as 1369 // soon as we start translating the stack. 1370 alphaNotifications = getNotificationsTopY() / mKeyguardStatusBar.getHeight(); 1371 } 1372 alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1); 1373 alphaNotifications = (float) Math.pow(alphaNotifications, 0.75); 1374 float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2); 1375 mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion)); 1376 mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications)); 1377 setQsTranslation(mQsExpansionHeight); 1378 } 1379 1380 private float getNotificationsTopY() { 1381 if (mNotificationStackScroller.getNotGoneChildCount() == 0) { 1382 return getExpandedHeight(); 1383 } 1384 return mNotificationStackScroller.getNotificationsTopY(); 1385 } 1386 1387 @Override 1388 protected void onExpandingStarted() { 1389 super.onExpandingStarted(); 1390 mNotificationStackScroller.onExpansionStarted(); 1391 mIsExpanding = true; 1392 mQsExpandedWhenExpandingStarted = mQsExpanded; 1393 if (mQsExpanded) { 1394 onQsExpansionStarted(); 1395 } 1396 } 1397 1398 @Override 1399 protected void onExpandingFinished() { 1400 super.onExpandingFinished(); 1401 mNotificationStackScroller.onExpansionStopped(); 1402 mIsExpanding = false; 1403 mScrollYOverride = -1; 1404 if (mExpandedHeight == 0f) { 1405 setListening(false); 1406 } else { 1407 setListening(true); 1408 } 1409 mTwoFingerQsExpand = false; 1410 mTwoFingerQsExpandPossible = false; 1411 } 1412 1413 private void setListening(boolean listening) { 1414 mHeader.setListening(listening); 1415 mKeyguardStatusBar.setListening(listening); 1416 mQsPanel.setListening(listening); 1417 } 1418 1419 @Override 1420 public void instantExpand() { 1421 super.instantExpand(); 1422 setListening(true); 1423 } 1424 1425 @Override 1426 protected void setOverExpansion(float overExpansion, boolean isPixels) { 1427 if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) { 1428 return; 1429 } 1430 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 1431 mNotificationStackScroller.setOnHeightChangedListener(null); 1432 if (isPixels) { 1433 mNotificationStackScroller.setOverScrolledPixels( 1434 overExpansion, true /* onTop */, false /* animate */); 1435 } else { 1436 mNotificationStackScroller.setOverScrollAmount( 1437 overExpansion, true /* onTop */, false /* animate */); 1438 } 1439 mNotificationStackScroller.setOnHeightChangedListener(this); 1440 } 1441 } 1442 1443 @Override 1444 protected void onTrackingStarted() { 1445 super.onTrackingStarted(); 1446 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1447 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1448 mAfforanceHelper.animateHideLeftRightIcon(); 1449 } 1450 if (mQsExpanded) { 1451 mTwoFingerQsExpand = true; 1452 } 1453 } 1454 1455 @Override 1456 protected void onTrackingStopped(boolean expand) { 1457 super.onTrackingStopped(expand); 1458 if (expand) { 1459 mNotificationStackScroller.setOverScrolledPixels( 1460 0.0f, true /* onTop */, true /* animate */); 1461 } 1462 if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1463 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1464 if (!mHintAnimationRunning) { 1465 mAfforanceHelper.reset(true); 1466 } 1467 } 1468 if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1469 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1470 KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon(); 1471 lockIcon.setImageAlpha(0.0f, true, 100, mFastOutLinearInterpolator, null); 1472 lockIcon.setImageScale(2.0f, true, 100, mFastOutLinearInterpolator); 1473 } 1474 } 1475 1476 @Override 1477 public void onHeightChanged(ExpandableView view) { 1478 1479 // Block update if we are in quick settings and just the top padding changed 1480 // (i.e. view == null). 1481 if (view == null && mQsExpanded) { 1482 return; 1483 } 1484 requestPanelHeightUpdate(); 1485 } 1486 1487 @Override 1488 public void onReset(ExpandableView view) { 1489 } 1490 1491 @Override 1492 public void onScrollChanged() { 1493 if (mQsExpanded) { 1494 requestScrollerTopPaddingUpdate(false /* animate */); 1495 requestPanelHeightUpdate(); 1496 } 1497 } 1498 1499 @Override 1500 protected void onConfigurationChanged(Configuration newConfig) { 1501 super.onConfigurationChanged(newConfig); 1502 mAfforanceHelper.onConfigurationChanged(); 1503 } 1504 1505 @Override 1506 public void onClick(View v) { 1507 if (v == mHeader) { 1508 onQsExpansionStarted(); 1509 if (mQsExpanded) { 1510 flingSettings(0 /* vel */, false /* expand */); 1511 } else if (mQsExpansionEnabled) { 1512 flingSettings(0 /* vel */, true /* expand */); 1513 } 1514 } 1515 } 1516 1517 @Override 1518 public void onAnimationToSideStarted(boolean rightPage) { 1519 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; 1520 mIsLaunchTransitionRunning = true; 1521 mLaunchAnimationEndRunnable = null; 1522 if (start) { 1523 mKeyguardBottomArea.launchPhone(); 1524 } else { 1525 mSecureCameraLaunchManager.startSecureCameraLaunch(); 1526 } 1527 mBlockTouches = true; 1528 } 1529 1530 @Override 1531 public void onAnimationToSideEnded() { 1532 mIsLaunchTransitionRunning = false; 1533 mIsLaunchTransitionFinished = true; 1534 if (mLaunchAnimationEndRunnable != null) { 1535 mLaunchAnimationEndRunnable.run(); 1536 mLaunchAnimationEndRunnable = null; 1537 } 1538 } 1539 1540 @Override 1541 protected void onEdgeClicked(boolean right) { 1542 if ((right && getRightIcon().getVisibility() != View.VISIBLE) 1543 || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) { 1544 return; 1545 } 1546 mHintAnimationRunning = true; 1547 mAfforanceHelper.startHintAnimation(right, new Runnable() { 1548 @Override 1549 public void run() { 1550 mHintAnimationRunning = false; 1551 mStatusBar.onHintFinished(); 1552 } 1553 }); 1554 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; 1555 if (start) { 1556 mStatusBar.onPhoneHintStarted(); 1557 } else { 1558 mStatusBar.onCameraHintStarted(); 1559 } 1560 } 1561 1562 @Override 1563 protected void startUnlockHintAnimation() { 1564 super.startUnlockHintAnimation(); 1565 startHighlightIconAnimation(getCenterIcon()); 1566 } 1567 1568 /** 1569 * Starts the highlight (making it fully opaque) animation on an icon. 1570 */ 1571 private void startHighlightIconAnimation(final KeyguardAffordanceView icon) { 1572 icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, 1573 mFastOutSlowInInterpolator, new Runnable() { 1574 @Override 1575 public void run() { 1576 icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, 1577 true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, 1578 mFastOutSlowInInterpolator, null); 1579 } 1580 }); 1581 } 1582 1583 @Override 1584 public float getPageWidth() { 1585 return getWidth(); 1586 } 1587 1588 @Override 1589 public void onSwipingStarted() { 1590 mSecureCameraLaunchManager.onSwipingStarted(); 1591 requestDisallowInterceptTouchEvent(true); 1592 mOnlyAffordanceInThisMotion = true; 1593 } 1594 1595 @Override 1596 public KeyguardAffordanceView getLeftIcon() { 1597 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1598 ? mKeyguardBottomArea.getCameraView() 1599 : mKeyguardBottomArea.getPhoneView(); 1600 } 1601 1602 @Override 1603 public KeyguardAffordanceView getCenterIcon() { 1604 return mKeyguardBottomArea.getLockIcon(); 1605 } 1606 1607 @Override 1608 public KeyguardAffordanceView getRightIcon() { 1609 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1610 ? mKeyguardBottomArea.getPhoneView() 1611 : mKeyguardBottomArea.getCameraView(); 1612 } 1613 1614 @Override 1615 public View getLeftPreview() { 1616 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1617 ? mKeyguardBottomArea.getCameraPreview() 1618 : mKeyguardBottomArea.getPhonePreview(); 1619 } 1620 1621 @Override 1622 public View getRightPreview() { 1623 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1624 ? mKeyguardBottomArea.getPhonePreview() 1625 : mKeyguardBottomArea.getCameraPreview(); 1626 } 1627 1628 @Override 1629 protected float getPeekHeight() { 1630 if (mNotificationStackScroller.getNotGoneChildCount() > 0) { 1631 return mNotificationStackScroller.getPeekHeight(); 1632 } else { 1633 return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR; 1634 } 1635 } 1636 1637 @Override 1638 protected float getCannedFlingDurationFactor() { 1639 if (mQsExpanded) { 1640 return 0.7f; 1641 } else { 1642 return 0.6f; 1643 } 1644 } 1645 1646 @Override 1647 protected boolean fullyExpandedClearAllVisible() { 1648 return mNotificationStackScroller.isDismissViewNotGone() 1649 && mNotificationStackScroller.isScrolledToBottom() && !mTwoFingerQsExpand; 1650 } 1651 1652 @Override 1653 protected boolean isClearAllVisible() { 1654 return mNotificationStackScroller.isDismissViewVisible(); 1655 } 1656 1657 @Override 1658 protected int getClearAllHeight() { 1659 return mNotificationStackScroller.getDismissViewHeight(); 1660 } 1661 1662 @Override 1663 protected boolean isTrackingBlocked() { 1664 return mConflictingQsExpansionGesture && mQsExpanded; 1665 } 1666 1667 public void notifyVisibleChildrenChanged() { 1668 if (mNotificationStackScroller.getNotGoneChildCount() != 0) { 1669 mReserveNotificationSpace.setVisibility(View.VISIBLE); 1670 } else { 1671 mReserveNotificationSpace.setVisibility(View.GONE); 1672 } 1673 } 1674 1675 public boolean isQsExpanded() { 1676 return mQsExpanded; 1677 } 1678 1679 public boolean isQsDetailShowing() { 1680 return mQsPanel.isShowingDetail(); 1681 } 1682 1683 public void closeQsDetail() { 1684 mQsPanel.closeDetail(); 1685 } 1686 1687 @Override 1688 public boolean shouldDelayChildPressedState() { 1689 return true; 1690 } 1691 1692 public boolean isLaunchTransitionFinished() { 1693 return mIsLaunchTransitionFinished; 1694 } 1695 1696 public boolean isLaunchTransitionRunning() { 1697 return mIsLaunchTransitionRunning; 1698 } 1699 1700 public void setLaunchTransitionEndRunnable(Runnable r) { 1701 mLaunchAnimationEndRunnable = r; 1702 } 1703 1704 public void setEmptyDragAmount(float amount) { 1705 float factor = 0.8f; 1706 if (mNotificationStackScroller.getNotGoneChildCount() > 0) { 1707 factor = 0.4f; 1708 } else if (!mStatusBar.hasActiveNotifications()) { 1709 factor = 0.4f; 1710 } 1711 mEmptyDragAmount = amount * factor; 1712 positionClockAndNotifications(); 1713 } 1714 1715 private static float interpolate(float t, float start, float end) { 1716 return (1 - t) * start + t * end; 1717 } 1718 1719 private void updateKeyguardStatusBarVisibility() { 1720 mKeyguardStatusBar.setVisibility(mKeyguardShowing && !mDozing ? VISIBLE : INVISIBLE); 1721 } 1722 1723 public void setDozing(boolean dozing) { 1724 if (dozing == mDozing) return; 1725 mDozing = dozing; 1726 if (mDozing) { 1727 setBackgroundColor(0xff000000); 1728 } else { 1729 setBackground(null); 1730 } 1731 updateKeyguardStatusBarVisibility(); 1732 } 1733 1734 public void setShadeEmpty(boolean shadeEmpty) { 1735 mShadeEmpty = shadeEmpty; 1736 updateEmptyShadeView(); 1737 } 1738 1739 private void updateEmptyShadeView() { 1740 1741 // Hide "No notifications" in QS. 1742 mNotificationStackScroller.updateEmptyShadeView(mShadeEmpty && !mQsExpanded); 1743 } 1744 1745 public void setQsScrimEnabled(boolean qsScrimEnabled) { 1746 boolean changed = mQsScrimEnabled != qsScrimEnabled; 1747 mQsScrimEnabled = qsScrimEnabled; 1748 if (changed) { 1749 updateQsState(); 1750 } 1751 } 1752 1753 public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) { 1754 mKeyguardUserSwitcher = keyguardUserSwitcher; 1755 } 1756 1757 private final Runnable mUpdateHeader = new Runnable() { 1758 @Override 1759 public void run() { 1760 mHeader.updateEverything(); 1761 } 1762 }; 1763 1764 public void onScreenTurnedOn() { 1765 mKeyguardStatusView.refreshTime(); 1766 } 1767} 1768