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