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