NotificationPanelView.java revision d09def75110ea9833e3985ce33a878a471a8a0dc
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 (!mQsTouchAboveFalsingThreshold && mStatusBarState == StatusBarState.KEYGUARD) { 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 float getQsExpansionFraction() { 562 return Math.min(1f, (mQsExpansionHeight - mQsMinExpansionHeight) 563 / (getTempQsMaxExpansion() - mQsMinExpansionHeight)); 564 } 565 566 @Override 567 public boolean onTouchEvent(MotionEvent event) { 568 if (mBlockTouches) { 569 return false; 570 } 571 resetDownStates(event); 572 if ((!mIsExpanding || mHintAnimationRunning) 573 && !mQsExpanded 574 && mStatusBar.getBarState() != StatusBarState.SHADE) { 575 mAfforanceHelper.onTouchEvent(event); 576 } 577 if (mOnlyAffordanceInThisMotion) { 578 return true; 579 } 580 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f 581 && mStatusBar.getBarState() != StatusBarState.KEYGUARD && !mQsExpanded 582 && mQsExpansionEnabled) { 583 584 // Down in the empty area while fully expanded - go to QS. 585 mQsTracking = true; 586 mConflictingQsExpansionGesture = true; 587 onQsExpansionStarted(); 588 mInitialHeightOnTouch = mQsExpansionHeight; 589 mInitialTouchY = event.getX(); 590 mInitialTouchX = event.getY(); 591 } 592 if (mExpandedHeight != 0) { 593 handleQsDown(event); 594 } 595 if (!mTwoFingerQsExpand && mQsTracking) { 596 onQsTouch(event); 597 if (!mConflictingQsExpansionGesture) { 598 return true; 599 } 600 } 601 if (event.getActionMasked() == MotionEvent.ACTION_CANCEL 602 || event.getActionMasked() == MotionEvent.ACTION_UP) { 603 mConflictingQsExpansionGesture = false; 604 } 605 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0 606 && mQsExpansionEnabled) { 607 mTwoFingerQsExpandPossible = true; 608 } 609 if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN 610 && event.getPointerCount() == 2 611 && event.getY(event.getActionIndex()) < mStatusBarMinHeight) { 612 mTwoFingerQsExpand = true; 613 requestPanelHeightUpdate(); 614 615 // Normally, we start listening when the panel is expanded, but here we need to start 616 // earlier so the state is already up to date when dragging down. 617 setListening(true); 618 } 619 super.onTouchEvent(event); 620 return true; 621 } 622 623 private boolean isInQsArea(float x, float y) { 624 return mStatusBarState != StatusBarState.SHADE 625 || y <= mNotificationStackScroller.getBottomMostNotificationBottom() 626 || y <= mQsContainer.getY() + mQsContainer.getHeight(); 627 } 628 629 private void handleQsDown(MotionEvent event) { 630 if (event.getActionMasked() == MotionEvent.ACTION_DOWN 631 && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) { 632 mQsTracking = true; 633 onQsExpansionStarted(); 634 mInitialHeightOnTouch = mQsExpansionHeight; 635 mInitialTouchY = event.getX(); 636 mInitialTouchX = event.getY(); 637 638 // If we interrupt an expansion gesture here, make sure to update the state correctly. 639 if (mIsExpanding) { 640 onExpandingFinished(); 641 } 642 } 643 } 644 645 @Override 646 protected boolean flingExpands(float vel, float vectorVel) { 647 boolean expands = super.flingExpands(vel, vectorVel); 648 649 // If we are already running a QS expansion, make sure that we keep the panel open. 650 if (mQsExpansionAnimator != null) { 651 expands = true; 652 } 653 return expands; 654 } 655 656 @Override 657 protected boolean hasConflictingGestures() { 658 return mStatusBar.getBarState() != StatusBarState.SHADE; 659 } 660 661 private void onQsTouch(MotionEvent event) { 662 int pointerIndex = event.findPointerIndex(mTrackingPointer); 663 if (pointerIndex < 0) { 664 pointerIndex = 0; 665 mTrackingPointer = event.getPointerId(pointerIndex); 666 } 667 final float y = event.getY(pointerIndex); 668 final float x = event.getX(pointerIndex); 669 670 switch (event.getActionMasked()) { 671 case MotionEvent.ACTION_DOWN: 672 mQsTracking = true; 673 mInitialTouchY = y; 674 mInitialTouchX = x; 675 onQsExpansionStarted(); 676 mInitialHeightOnTouch = mQsExpansionHeight; 677 initVelocityTracker(); 678 trackMovement(event); 679 break; 680 681 case MotionEvent.ACTION_POINTER_UP: 682 final int upPointer = event.getPointerId(event.getActionIndex()); 683 if (mTrackingPointer == upPointer) { 684 // gesture is ongoing, find a new pointer to track 685 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 686 final float newY = event.getY(newIndex); 687 final float newX = event.getX(newIndex); 688 mTrackingPointer = event.getPointerId(newIndex); 689 mInitialHeightOnTouch = mQsExpansionHeight; 690 mInitialTouchY = newY; 691 mInitialTouchX = newX; 692 } 693 break; 694 695 case MotionEvent.ACTION_MOVE: 696 final float h = y - mInitialTouchY; 697 setQsExpansion(h + mInitialHeightOnTouch); 698 if (h >= mQsFalsingThreshold) { 699 mQsTouchAboveFalsingThreshold = true; 700 } 701 trackMovement(event); 702 break; 703 704 case MotionEvent.ACTION_UP: 705 case MotionEvent.ACTION_CANCEL: 706 mQsTracking = false; 707 mTrackingPointer = -1; 708 trackMovement(event); 709 float fraction = getQsExpansionFraction(); 710 if ((fraction != 0f || y >= mInitialTouchY) 711 && (fraction != 1f || y <= mInitialTouchY)) { 712 flingQsWithCurrentVelocity(); 713 } else { 714 mScrollYOverride = -1; 715 } 716 if (mVelocityTracker != null) { 717 mVelocityTracker.recycle(); 718 mVelocityTracker = null; 719 } 720 break; 721 } 722 } 723 724 @Override 725 public void onOverscrolled(float lastTouchX, float lastTouchY, int amount) { 726 if (mIntercepting && shouldQuickSettingsIntercept(lastTouchX, lastTouchY, 727 -1 /* yDiff: Not relevant here */)) { 728 onQsExpansionStarted(amount); 729 mInitialHeightOnTouch = mQsExpansionHeight; 730 mInitialTouchY = mLastTouchY; 731 mInitialTouchX = mLastTouchX; 732 mQsTracking = true; 733 } 734 } 735 736 @Override 737 public void onOverscrollTopChanged(float amount, boolean isRubberbanded) { 738 cancelAnimation(); 739 if (!mQsExpansionEnabled) { 740 amount = 0f; 741 } 742 float rounded = amount >= 1f ? amount : 0f; 743 mStackScrollerOverscrolling = rounded != 0f && isRubberbanded; 744 mQsExpansionFromOverscroll = rounded != 0f; 745 mLastOverscroll = rounded; 746 updateQsState(); 747 setQsExpansion(mQsMinExpansionHeight + rounded); 748 } 749 750 @Override 751 public void flingTopOverscroll(float velocity, boolean open) { 752 mLastOverscroll = 0f; 753 setQsExpansion(mQsExpansionHeight); 754 flingSettings(!mQsExpansionEnabled && open ? 0f : velocity, open && mQsExpansionEnabled, 755 new Runnable() { 756 @Override 757 public void run() { 758 mStackScrollerOverscrolling = false; 759 mQsExpansionFromOverscroll = false; 760 updateQsState(); 761 } 762 }); 763 } 764 765 private void onQsExpansionStarted() { 766 onQsExpansionStarted(0); 767 } 768 769 private void onQsExpansionStarted(int overscrollAmount) { 770 cancelAnimation(); 771 772 // Reset scroll position and apply that position to the expanded height. 773 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 774 if (mScrollView.getScrollY() != 0) { 775 mScrollYOverride = mScrollView.getScrollY(); 776 } 777 mScrollView.scrollTo(0, 0); 778 setQsExpansion(height); 779 } 780 781 private void setQsExpanded(boolean expanded) { 782 boolean changed = mQsExpanded != expanded; 783 if (changed) { 784 mQsExpanded = expanded; 785 updateQsState(); 786 requestPanelHeightUpdate(); 787 mNotificationStackScroller.setInterceptDelegateEnabled(expanded); 788 mStatusBar.setQsExpanded(expanded); 789 } 790 } 791 792 public void setBarState(int statusBarState, boolean keyguardFadingAway, 793 boolean goingToFullShade) { 794 boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD 795 || statusBarState == StatusBarState.SHADE_LOCKED; 796 if (!mKeyguardShowing && keyguardShowing) { 797 setQsTranslation(mQsExpansionHeight); 798 mHeader.setTranslationY(0f); 799 } 800 setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade); 801 setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); 802 if (goingToFullShade) { 803 animateKeyguardStatusBarOut(); 804 } else { 805 mKeyguardStatusBar.setAlpha(1f); 806 mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE); 807 } 808 mStatusBarState = statusBarState; 809 mKeyguardShowing = keyguardShowing; 810 updateQsState(); 811 if (goingToFullShade) { 812 animateHeaderSlidingIn(); 813 } 814 } 815 816 private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() { 817 @Override 818 public void run() { 819 mKeyguardStatusViewAnimating = false; 820 mKeyguardStatusView.setVisibility(View.GONE); 821 } 822 }; 823 824 private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() { 825 @Override 826 public void run() { 827 mKeyguardStatusViewAnimating = false; 828 } 829 }; 830 831 private final Animator.AnimatorListener mAnimateHeaderSlidingInListener 832 = new AnimatorListenerAdapter() { 833 @Override 834 public void onAnimationEnd(Animator animation) { 835 mHeaderAnimatingIn = false; 836 mQsContainerAnimator = null; 837 mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater); 838 } 839 }; 840 841 private final OnLayoutChangeListener mQsContainerAnimatorUpdater 842 = new OnLayoutChangeListener() { 843 @Override 844 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 845 int oldTop, int oldRight, int oldBottom) { 846 int oldHeight = oldBottom - oldTop; 847 int height = bottom - top; 848 if (height != oldHeight && mQsContainerAnimator != null) { 849 PropertyValuesHolder[] values = mQsContainerAnimator.getValues(); 850 float newEndValue = mHeader.getCollapsedHeight() + mQsPeekHeight - height - top; 851 float newStartValue = -height - top; 852 values[0].setFloatValues(newStartValue, newEndValue); 853 mQsContainerAnimator.setCurrentPlayTime(mQsContainerAnimator.getCurrentPlayTime()); 854 } 855 } 856 }; 857 858 private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn 859 = new ViewTreeObserver.OnPreDrawListener() { 860 @Override 861 public boolean onPreDraw() { 862 getViewTreeObserver().removeOnPreDrawListener(this); 863 mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight); 864 mHeader.animate() 865 .translationY(0f) 866 .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()) 867 .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) 868 .setInterpolator(mFastOutSlowInInterpolator) 869 .start(); 870 mQsContainer.setY(-mQsContainer.getHeight()); 871 mQsContainerAnimator = ObjectAnimator.ofFloat(mQsContainer, View.TRANSLATION_Y, 872 mQsContainer.getTranslationY(), 873 mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight() 874 - mQsContainer.getTop()); 875 mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()); 876 mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE); 877 mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator); 878 mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener); 879 mQsContainerAnimator.start(); 880 mQsContainer.addOnLayoutChangeListener(mQsContainerAnimatorUpdater); 881 return true; 882 } 883 }; 884 885 private void animateHeaderSlidingIn() { 886 mHeaderAnimatingIn = true; 887 getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); 888 889 } 890 891 private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() { 892 @Override 893 public void run() { 894 mKeyguardStatusBar.setVisibility(View.INVISIBLE); 895 } 896 }; 897 898 private void animateKeyguardStatusBarOut() { 899 mKeyguardStatusBar.animate() 900 .alpha(0f) 901 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 902 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 903 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 904 .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable) 905 .start(); 906 } 907 908 private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() { 909 @Override 910 public void run() { 911 mKeyguardBottomArea.setVisibility(View.GONE); 912 } 913 }; 914 915 private void setKeyguardBottomAreaVisibility(int statusBarState, 916 boolean goingToFullShade) { 917 if (goingToFullShade) { 918 mKeyguardBottomArea.animate().cancel(); 919 mKeyguardBottomArea.animate() 920 .alpha(0f) 921 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 922 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 923 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 924 .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable) 925 .start(); 926 } else if (statusBarState == StatusBarState.KEYGUARD 927 || statusBarState == StatusBarState.SHADE_LOCKED) { 928 mKeyguardBottomArea.animate().cancel(); 929 mKeyguardBottomArea.setVisibility(View.VISIBLE); 930 mKeyguardBottomArea.setAlpha(1f); 931 } else { 932 mKeyguardBottomArea.animate().cancel(); 933 mKeyguardBottomArea.setVisibility(View.GONE); 934 mKeyguardBottomArea.setAlpha(1f); 935 } 936 } 937 938 private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway, 939 boolean goingToFullShade) { 940 if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD 941 && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) { 942 mKeyguardStatusView.animate().cancel(); 943 mKeyguardStatusViewAnimating = true; 944 mKeyguardStatusView.animate() 945 .alpha(0f) 946 .setStartDelay(0) 947 .setDuration(160) 948 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 949 .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable); 950 if (keyguardFadingAway) { 951 mKeyguardStatusView.animate() 952 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 953 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 954 .start(); 955 } 956 } else if (mStatusBarState == StatusBarState.SHADE_LOCKED 957 && statusBarState == StatusBarState.KEYGUARD) { 958 mKeyguardStatusView.animate().cancel(); 959 mKeyguardStatusView.setVisibility(View.VISIBLE); 960 mKeyguardStatusViewAnimating = true; 961 mKeyguardStatusView.setAlpha(0f); 962 mKeyguardStatusView.animate() 963 .alpha(1f) 964 .setStartDelay(0) 965 .setDuration(320) 966 .setInterpolator(PhoneStatusBar.ALPHA_IN) 967 .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable); 968 } else if (statusBarState == StatusBarState.KEYGUARD) { 969 mKeyguardStatusView.animate().cancel(); 970 mKeyguardStatusView.setVisibility(View.VISIBLE); 971 mKeyguardStatusView.setAlpha(1f); 972 } else { 973 mKeyguardStatusView.animate().cancel(); 974 mKeyguardStatusView.setVisibility(View.GONE); 975 mKeyguardStatusView.setAlpha(1f); 976 } 977 } 978 979 private void updateQsState() { 980 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; 981 mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE); 982 mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling)); 983 mNotificationStackScroller.setScrollingEnabled( 984 mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded 985 || mQsExpansionFromOverscroll)); 986 mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 987 mQsContainer.setVisibility( 988 mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE); 989 mScrollView.setTouchEnabled(mQsExpanded); 990 updateEmptyShadeView(); 991 mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded 992 && !mStackScrollerOverscrolling && mQsScrimEnabled 993 ? View.VISIBLE 994 : View.INVISIBLE); 995 if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) { 996 mKeyguardUserSwitcher.hide(true /* animate */); 997 } 998 } 999 1000 private void setQsExpansion(float height) { 1001 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); 1002 mQsFullyExpanded = height == mQsMaxExpansionHeight; 1003 if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) { 1004 setQsExpanded(true); 1005 } else if (height <= mQsMinExpansionHeight && mQsExpanded) { 1006 setQsExpanded(false); 1007 if (mLastAnnouncementWasQuickSettings && !mTracking) { 1008 announceForAccessibility(getKeyguardOrLockScreenString()); 1009 mLastAnnouncementWasQuickSettings = false; 1010 } 1011 } 1012 mQsExpansionHeight = height; 1013 mHeader.setExpansion(getHeaderExpansionFraction()); 1014 setQsTranslation(height); 1015 requestScrollerTopPaddingUpdate(false /* animate */); 1016 updateNotificationScrim(height); 1017 if (mKeyguardShowing) { 1018 updateHeaderKeyguard(); 1019 } 1020 if (mStatusBarState == StatusBarState.SHADE && mQsExpanded 1021 && !mStackScrollerOverscrolling && mQsScrimEnabled) { 1022 mQsNavbarScrim.setAlpha(getQsExpansionFraction()); 1023 } 1024 1025 // Upon initialisation when we are not layouted yet we don't want to announce that we are 1026 // fully expanded, hence the != 0.0f check. 1027 if (height != 0.0f && mQsFullyExpanded && !mLastAnnouncementWasQuickSettings) { 1028 announceForAccessibility(getContext().getString( 1029 R.string.accessibility_desc_quick_settings)); 1030 mLastAnnouncementWasQuickSettings = true; 1031 } 1032 } 1033 1034 private String getKeyguardOrLockScreenString() { 1035 if (mStatusBarState == StatusBarState.KEYGUARD) { 1036 return getContext().getString(R.string.accessibility_desc_lock_screen); 1037 } else { 1038 return getContext().getString(R.string.accessibility_desc_notification_shade); 1039 } 1040 } 1041 1042 private void updateNotificationScrim(float height) { 1043 int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance; 1044 float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance); 1045 progress = Math.max(0.0f, Math.min(progress, 1.0f)); 1046 mNotificationStackScroller.setScrimAlpha(progress); 1047 } 1048 1049 private float getHeaderExpansionFraction() { 1050 if (!mKeyguardShowing) { 1051 return getQsExpansionFraction(); 1052 } else { 1053 return 1f; 1054 } 1055 } 1056 1057 private void setQsTranslation(float height) { 1058 if (!mHeaderAnimatingIn) { 1059 mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation()); 1060 } 1061 if (mKeyguardShowing) { 1062 mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0)); 1063 } 1064 } 1065 1066 private float calculateQsTopPadding() { 1067 // We can only do the smoother transition on Keyguard when we also are not collapsing from a 1068 // scrolled quick settings. 1069 if (mKeyguardShowing && mScrollYOverride == -1) { 1070 return interpolate(getQsExpansionFraction(), 1071 mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding, 1072 mQsMaxExpansionHeight); 1073 } else { 1074 return mQsExpansionHeight; 1075 } 1076 } 1077 1078 private void requestScrollerTopPaddingUpdate(boolean animate) { 1079 mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(), 1080 mScrollView.getScrollY(), 1081 mAnimateNextTopPaddingChange || animate); 1082 mAnimateNextTopPaddingChange = false; 1083 } 1084 1085 private void trackMovement(MotionEvent event) { 1086 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 1087 mLastTouchX = event.getX(); 1088 mLastTouchY = event.getY(); 1089 } 1090 1091 private void initVelocityTracker() { 1092 if (mVelocityTracker != null) { 1093 mVelocityTracker.recycle(); 1094 } 1095 mVelocityTracker = VelocityTracker.obtain(); 1096 } 1097 1098 private float getCurrentVelocity() { 1099 if (mVelocityTracker == null) { 1100 return 0; 1101 } 1102 mVelocityTracker.computeCurrentVelocity(1000); 1103 return mVelocityTracker.getYVelocity(); 1104 } 1105 1106 private void cancelAnimation() { 1107 if (mQsExpansionAnimator != null) { 1108 mQsExpansionAnimator.cancel(); 1109 } 1110 } 1111 1112 private void flingSettings(float vel, boolean expand) { 1113 flingSettings(vel, expand, null); 1114 } 1115 1116 private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) { 1117 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 1118 if (target == mQsExpansionHeight) { 1119 mScrollYOverride = -1; 1120 if (onFinishRunnable != null) { 1121 onFinishRunnable.run(); 1122 } 1123 return; 1124 } 1125 mScrollView.setBlockFlinging(true); 1126 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 1127 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 1128 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1129 @Override 1130 public void onAnimationUpdate(ValueAnimator animation) { 1131 setQsExpansion((Float) animation.getAnimatedValue()); 1132 } 1133 }); 1134 animator.addListener(new AnimatorListenerAdapter() { 1135 @Override 1136 public void onAnimationEnd(Animator animation) { 1137 mScrollView.setBlockFlinging(false); 1138 mScrollYOverride = -1; 1139 mQsExpansionAnimator = null; 1140 if (onFinishRunnable != null) { 1141 onFinishRunnable.run(); 1142 } 1143 } 1144 }); 1145 animator.start(); 1146 mQsExpansionAnimator = animator; 1147 mQsAnimatorExpand = expand; 1148 } 1149 1150 /** 1151 * @return Whether we should intercept a gesture to open Quick Settings. 1152 */ 1153 private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { 1154 if (!mQsExpansionEnabled) { 1155 return false; 1156 } 1157 View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader; 1158 boolean onHeader = x >= header.getLeft() && x <= header.getRight() 1159 && y >= header.getTop() && y <= header.getBottom(); 1160 if (mQsExpanded) { 1161 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y); 1162 } else { 1163 return onHeader; 1164 } 1165 } 1166 1167 @Override 1168 protected boolean isScrolledToBottom() { 1169 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 1170 return true; 1171 } 1172 if (!isInSettings()) { 1173 return mNotificationStackScroller.isScrolledToBottom(); 1174 } else { 1175 return mScrollView.isScrolledToBottom(); 1176 } 1177 } 1178 1179 @Override 1180 protected int getMaxPanelHeight() { 1181 int min = mStatusBarMinHeight; 1182 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD 1183 && mNotificationStackScroller.getNotGoneChildCount() == 0) { 1184 int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount()) 1185 * HEADER_RUBBERBAND_FACTOR); 1186 min = Math.max(min, minHeight); 1187 } 1188 int maxHeight; 1189 if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) { 1190 maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade()); 1191 } else { 1192 maxHeight = calculatePanelHeightShade(); 1193 } 1194 maxHeight = Math.max(maxHeight, min); 1195 return maxHeight; 1196 } 1197 1198 private boolean isInSettings() { 1199 return mQsExpanded; 1200 } 1201 1202 @Override 1203 protected void onHeightUpdated(float expandedHeight) { 1204 if (!mQsExpanded) { 1205 positionClockAndNotifications(); 1206 } 1207 if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null 1208 && !mQsExpansionFromOverscroll) { 1209 float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding() 1210 + mNotificationStackScroller.getMinStackHeight() 1211 + mNotificationStackScroller.getNotificationTopPadding(); 1212 float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); 1213 float t = (expandedHeight - panelHeightQsCollapsed) 1214 / (panelHeightQsExpanded - panelHeightQsCollapsed); 1215 1216 setQsExpansion(mQsMinExpansionHeight 1217 + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight)); 1218 } 1219 mNotificationStackScroller.setStackHeight(expandedHeight); 1220 updateHeader(); 1221 updateUnlockIcon(); 1222 updateNotificationTranslucency(); 1223 } 1224 1225 /** 1226 * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when 1227 * collapsing QS / the panel when QS was scrolled 1228 */ 1229 private int getTempQsMaxExpansion() { 1230 int qsTempMaxExpansion = mQsMaxExpansionHeight; 1231 if (mScrollYOverride != -1) { 1232 qsTempMaxExpansion -= mScrollYOverride; 1233 } 1234 return qsTempMaxExpansion; 1235 } 1236 1237 private int calculatePanelHeightShade() { 1238 int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin(); 1239 int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin 1240 - mTopPaddingAdjustment; 1241 maxHeight += mNotificationStackScroller.getTopPaddingOverflow(); 1242 return maxHeight; 1243 } 1244 1245 private int calculatePanelHeightQsExpanded() { 1246 float notificationHeight = mNotificationStackScroller.getHeight() 1247 - mNotificationStackScroller.getEmptyBottomMargin() 1248 - mNotificationStackScroller.getTopPadding(); 1249 float totalHeight = mQsMaxExpansionHeight + notificationHeight 1250 + mNotificationStackScroller.getNotificationTopPadding(); 1251 if (totalHeight > mNotificationStackScroller.getHeight()) { 1252 float fullyCollapsedHeight = mQsMaxExpansionHeight 1253 + mNotificationStackScroller.getMinStackHeight() 1254 + mNotificationStackScroller.getNotificationTopPadding() 1255 - getScrollViewScrollY(); 1256 totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight()); 1257 } 1258 return (int) totalHeight; 1259 } 1260 1261 private int getScrollViewScrollY() { 1262 if (mScrollYOverride != -1) { 1263 return mScrollYOverride; 1264 } else { 1265 return mScrollView.getScrollY(); 1266 } 1267 } 1268 private void updateNotificationTranslucency() { 1269 float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight()) 1270 / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize() 1271 + mNotificationStackScroller.getCollapseSecondCardPadding()); 1272 alpha = Math.max(0, Math.min(alpha, 1)); 1273 alpha = (float) Math.pow(alpha, 0.75); 1274 if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) { 1275 mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null); 1276 } else if (alpha == 1f 1277 && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) { 1278 mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null); 1279 } 1280 mNotificationStackScroller.setAlpha(alpha); 1281 } 1282 1283 @Override 1284 protected float getOverExpansionAmount() { 1285 return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */); 1286 } 1287 1288 @Override 1289 protected float getOverExpansionPixels() { 1290 return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */); 1291 } 1292 1293 private void updateUnlockIcon() { 1294 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1295 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1296 boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance; 1297 KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon(); 1298 if (active && !mUnlockIconActive && mTracking) { 1299 lockIcon.setImageAlpha(1.0f, true, 150, mFastOutLinearInterpolator, null); 1300 lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150, 1301 mFastOutLinearInterpolator); 1302 } else if (!active && mUnlockIconActive && mTracking) { 1303 lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true, 1304 150, mFastOutLinearInterpolator, null); 1305 lockIcon.setImageScale(1.0f, true, 150, 1306 mFastOutLinearInterpolator); 1307 } 1308 mUnlockIconActive = active; 1309 } 1310 } 1311 1312 /** 1313 * Hides the header when notifications are colliding with it. 1314 */ 1315 private void updateHeader() { 1316 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1317 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1318 updateHeaderKeyguard(); 1319 } else { 1320 updateHeaderShade(); 1321 } 1322 1323 } 1324 1325 private void updateHeaderShade() { 1326 if (!mHeaderAnimatingIn) { 1327 mHeader.setTranslationY(getHeaderTranslation()); 1328 } 1329 setQsTranslation(mQsExpansionHeight); 1330 } 1331 1332 private float getHeaderTranslation() { 1333 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1334 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1335 return 0; 1336 } 1337 if (mNotificationStackScroller.getNotGoneChildCount() == 0) { 1338 if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) { 1339 return 0; 1340 } else { 1341 return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight; 1342 } 1343 } 1344 return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR; 1345 } 1346 1347 private void updateHeaderKeyguard() { 1348 float alphaNotifications; 1349 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 1350 1351 // When on Keyguard, we hide the header as soon as the top card of the notification 1352 // stack scroller is close enough (collision distance) to the bottom of the header. 1353 alphaNotifications = getNotificationsTopY() 1354 / 1355 (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance); 1356 } else { 1357 1358 // In SHADE_LOCKED, the top card is already really close to the header. Hide it as 1359 // soon as we start translating the stack. 1360 alphaNotifications = getNotificationsTopY() / mKeyguardStatusBar.getHeight(); 1361 } 1362 alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1); 1363 alphaNotifications = (float) Math.pow(alphaNotifications, 0.75); 1364 float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2); 1365 mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion)); 1366 mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications)); 1367 setQsTranslation(mQsExpansionHeight); 1368 } 1369 1370 private float getNotificationsTopY() { 1371 if (mNotificationStackScroller.getNotGoneChildCount() == 0) { 1372 return getExpandedHeight(); 1373 } 1374 return mNotificationStackScroller.getNotificationsTopY(); 1375 } 1376 1377 @Override 1378 protected void onExpandingStarted() { 1379 super.onExpandingStarted(); 1380 mNotificationStackScroller.onExpansionStarted(); 1381 mIsExpanding = true; 1382 mQsExpandedWhenExpandingStarted = mQsExpanded; 1383 if (mQsExpanded) { 1384 onQsExpansionStarted(); 1385 } 1386 } 1387 1388 @Override 1389 protected void onExpandingFinished() { 1390 super.onExpandingFinished(); 1391 mNotificationStackScroller.onExpansionStopped(); 1392 mIsExpanding = false; 1393 mScrollYOverride = -1; 1394 if (mExpandedHeight == 0f) { 1395 setListening(false); 1396 } else { 1397 setListening(true); 1398 } 1399 mTwoFingerQsExpand = false; 1400 mTwoFingerQsExpandPossible = false; 1401 } 1402 1403 private void setListening(boolean listening) { 1404 mHeader.setListening(listening); 1405 mKeyguardStatusBar.setListening(listening); 1406 mQsPanel.setListening(listening); 1407 } 1408 1409 @Override 1410 public void instantExpand() { 1411 super.instantExpand(); 1412 setListening(true); 1413 } 1414 1415 @Override 1416 protected void setOverExpansion(float overExpansion, boolean isPixels) { 1417 if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) { 1418 return; 1419 } 1420 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 1421 mNotificationStackScroller.setOnHeightChangedListener(null); 1422 if (isPixels) { 1423 mNotificationStackScroller.setOverScrolledPixels( 1424 overExpansion, true /* onTop */, false /* animate */); 1425 } else { 1426 mNotificationStackScroller.setOverScrollAmount( 1427 overExpansion, true /* onTop */, false /* animate */); 1428 } 1429 mNotificationStackScroller.setOnHeightChangedListener(this); 1430 } 1431 } 1432 1433 @Override 1434 protected void onTrackingStarted() { 1435 super.onTrackingStarted(); 1436 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1437 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1438 mAfforanceHelper.animateHideLeftRightIcon(); 1439 } 1440 if (mQsExpanded) { 1441 mTwoFingerQsExpand = true; 1442 } 1443 } 1444 1445 @Override 1446 protected void onTrackingStopped(boolean expand) { 1447 super.onTrackingStopped(expand); 1448 if (expand) { 1449 mNotificationStackScroller.setOverScrolledPixels( 1450 0.0f, true /* onTop */, true /* animate */); 1451 } 1452 if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1453 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1454 if (!mHintAnimationRunning) { 1455 mAfforanceHelper.reset(true); 1456 } 1457 } 1458 if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1459 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1460 KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon(); 1461 lockIcon.setImageAlpha(0.0f, true, 100, mFastOutLinearInterpolator, null); 1462 lockIcon.setImageScale(2.0f, true, 100, mFastOutLinearInterpolator); 1463 } 1464 } 1465 1466 @Override 1467 public void onHeightChanged(ExpandableView view) { 1468 1469 // Block update if we are in quick settings and just the top padding changed 1470 // (i.e. view == null). 1471 if (view == null && mQsExpanded) { 1472 return; 1473 } 1474 requestPanelHeightUpdate(); 1475 } 1476 1477 @Override 1478 public void onReset(ExpandableView view) { 1479 } 1480 1481 @Override 1482 public void onScrollChanged() { 1483 if (mQsExpanded) { 1484 requestScrollerTopPaddingUpdate(false /* animate */); 1485 requestPanelHeightUpdate(); 1486 } 1487 } 1488 1489 @Override 1490 protected void onConfigurationChanged(Configuration newConfig) { 1491 super.onConfigurationChanged(newConfig); 1492 mAfforanceHelper.onConfigurationChanged(); 1493 } 1494 1495 @Override 1496 public void onClick(View v) { 1497 if (v == mHeader) { 1498 onQsExpansionStarted(); 1499 if (mQsExpanded) { 1500 flingSettings(0 /* vel */, false /* expand */); 1501 } else if (mQsExpansionEnabled) { 1502 flingSettings(0 /* vel */, true /* expand */); 1503 } 1504 } 1505 } 1506 1507 @Override 1508 public void onAnimationToSideStarted(boolean rightPage) { 1509 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; 1510 mIsLaunchTransitionRunning = true; 1511 mLaunchAnimationEndRunnable = null; 1512 if (start) { 1513 mKeyguardBottomArea.launchPhone(); 1514 } else { 1515 mSecureCameraLaunchManager.startSecureCameraLaunch(); 1516 } 1517 mBlockTouches = true; 1518 } 1519 1520 @Override 1521 public void onAnimationToSideEnded() { 1522 mIsLaunchTransitionRunning = false; 1523 mIsLaunchTransitionFinished = true; 1524 if (mLaunchAnimationEndRunnable != null) { 1525 mLaunchAnimationEndRunnable.run(); 1526 mLaunchAnimationEndRunnable = null; 1527 } 1528 } 1529 1530 @Override 1531 protected void onEdgeClicked(boolean right) { 1532 if ((right && getRightIcon().getVisibility() != View.VISIBLE) 1533 || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) { 1534 return; 1535 } 1536 mHintAnimationRunning = true; 1537 mAfforanceHelper.startHintAnimation(right, new Runnable() { 1538 @Override 1539 public void run() { 1540 mHintAnimationRunning = false; 1541 mStatusBar.onHintFinished(); 1542 } 1543 }); 1544 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; 1545 if (start) { 1546 mStatusBar.onPhoneHintStarted(); 1547 } else { 1548 mStatusBar.onCameraHintStarted(); 1549 } 1550 } 1551 1552 @Override 1553 protected void startUnlockHintAnimation() { 1554 super.startUnlockHintAnimation(); 1555 startHighlightIconAnimation(getCenterIcon()); 1556 } 1557 1558 /** 1559 * Starts the highlight (making it fully opaque) animation on an icon. 1560 */ 1561 private void startHighlightIconAnimation(final KeyguardAffordanceView icon) { 1562 icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, 1563 mFastOutSlowInInterpolator, new Runnable() { 1564 @Override 1565 public void run() { 1566 icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, 1567 true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, 1568 mFastOutSlowInInterpolator, null); 1569 } 1570 }); 1571 } 1572 1573 @Override 1574 public float getPageWidth() { 1575 return getWidth(); 1576 } 1577 1578 @Override 1579 public void onSwipingStarted() { 1580 mSecureCameraLaunchManager.onSwipingStarted(); 1581 requestDisallowInterceptTouchEvent(true); 1582 mOnlyAffordanceInThisMotion = true; 1583 } 1584 1585 @Override 1586 public KeyguardAffordanceView getLeftIcon() { 1587 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1588 ? mKeyguardBottomArea.getCameraView() 1589 : mKeyguardBottomArea.getPhoneView(); 1590 } 1591 1592 @Override 1593 public KeyguardAffordanceView getCenterIcon() { 1594 return mKeyguardBottomArea.getLockIcon(); 1595 } 1596 1597 @Override 1598 public KeyguardAffordanceView getRightIcon() { 1599 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1600 ? mKeyguardBottomArea.getPhoneView() 1601 : mKeyguardBottomArea.getCameraView(); 1602 } 1603 1604 @Override 1605 public View getLeftPreview() { 1606 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1607 ? mKeyguardBottomArea.getCameraPreview() 1608 : mKeyguardBottomArea.getPhonePreview(); 1609 } 1610 1611 @Override 1612 public View getRightPreview() { 1613 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1614 ? mKeyguardBottomArea.getPhonePreview() 1615 : mKeyguardBottomArea.getCameraPreview(); 1616 } 1617 1618 @Override 1619 protected float getPeekHeight() { 1620 if (mNotificationStackScroller.getNotGoneChildCount() > 0) { 1621 return mNotificationStackScroller.getPeekHeight(); 1622 } else { 1623 return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR; 1624 } 1625 } 1626 1627 @Override 1628 protected float getCannedFlingDurationFactor() { 1629 if (mQsExpanded) { 1630 return 0.7f; 1631 } else { 1632 return 0.6f; 1633 } 1634 } 1635 1636 @Override 1637 protected boolean fullyExpandedClearAllVisible() { 1638 return mNotificationStackScroller.isDismissViewNotGone() 1639 && mNotificationStackScroller.isScrolledToBottom() && !mTwoFingerQsExpand; 1640 } 1641 1642 @Override 1643 protected boolean isClearAllVisible() { 1644 return mNotificationStackScroller.isDismissViewVisible(); 1645 } 1646 1647 @Override 1648 protected int getClearAllHeight() { 1649 return mNotificationStackScroller.getDismissViewHeight(); 1650 } 1651 1652 @Override 1653 protected boolean isTrackingBlocked() { 1654 return mConflictingQsExpansionGesture && mQsExpanded; 1655 } 1656 1657 public void notifyVisibleChildrenChanged() { 1658 if (mNotificationStackScroller.getNotGoneChildCount() != 0) { 1659 mReserveNotificationSpace.setVisibility(View.VISIBLE); 1660 } else { 1661 mReserveNotificationSpace.setVisibility(View.GONE); 1662 } 1663 } 1664 1665 public boolean isQsExpanded() { 1666 return mQsExpanded; 1667 } 1668 1669 public boolean isQsDetailShowing() { 1670 return mQsPanel.isShowingDetail(); 1671 } 1672 1673 public void closeQsDetail() { 1674 mQsPanel.closeDetail(); 1675 } 1676 1677 @Override 1678 public boolean shouldDelayChildPressedState() { 1679 return true; 1680 } 1681 1682 public boolean isLaunchTransitionFinished() { 1683 return mIsLaunchTransitionFinished; 1684 } 1685 1686 public boolean isLaunchTransitionRunning() { 1687 return mIsLaunchTransitionRunning; 1688 } 1689 1690 public void setLaunchTransitionEndRunnable(Runnable r) { 1691 mLaunchAnimationEndRunnable = r; 1692 } 1693 1694 public void setEmptyDragAmount(float amount) { 1695 float factor = 1f; 1696 if (mNotificationStackScroller.getNotGoneChildCount() > 0) { 1697 factor = 0.6f; 1698 } else if (!mStatusBar.hasActiveNotifications()) { 1699 factor = 0.4f; 1700 } 1701 mEmptyDragAmount = amount * factor; 1702 positionClockAndNotifications(); 1703 } 1704 1705 private static float interpolate(float t, float start, float end) { 1706 return (1 - t) * start + t * end; 1707 } 1708 1709 private void updateKeyguardStatusBarVisibility() { 1710 mKeyguardStatusBar.setVisibility(mKeyguardShowing && !mDozing ? VISIBLE : INVISIBLE); 1711 } 1712 1713 public void setDozing(boolean dozing) { 1714 if (dozing == mDozing) return; 1715 mDozing = dozing; 1716 if (mDozing) { 1717 setBackgroundColor(0xff000000); 1718 } else { 1719 setBackground(null); 1720 } 1721 updateKeyguardStatusBarVisibility(); 1722 } 1723 1724 public void setShadeEmpty(boolean shadeEmpty) { 1725 mShadeEmpty = shadeEmpty; 1726 updateEmptyShadeView(); 1727 } 1728 1729 private void updateEmptyShadeView() { 1730 1731 // Hide "No notifications" in QS. 1732 mNotificationStackScroller.updateEmptyShadeView(mShadeEmpty && !mQsExpanded); 1733 } 1734 1735 public void setQsScrimEnabled(boolean qsScrimEnabled) { 1736 boolean changed = mQsScrimEnabled != qsScrimEnabled; 1737 mQsScrimEnabled = qsScrimEnabled; 1738 if (changed) { 1739 updateQsState(); 1740 } 1741 } 1742 1743 public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) { 1744 mKeyguardUserSwitcher = keyguardUserSwitcher; 1745 } 1746 1747 private final Runnable mUpdateHeader = new Runnable() { 1748 @Override 1749 public void run() { 1750 mHeader.updateEverything(); 1751 } 1752 }; 1753 1754 public void onScreenTurnedOn() { 1755 mKeyguardStatusView.refreshTime(); 1756 } 1757} 1758