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