NotificationPanelView.java revision b7a3303f807ee01bbe405f4153a2285e55c0b6e3
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; 39 40import com.android.systemui.R; 41import com.android.systemui.qs.QSPanel; 42import com.android.systemui.statusbar.ExpandableView; 43import com.android.systemui.statusbar.FlingAnimationUtils; 44import com.android.systemui.statusbar.GestureRecorder; 45import com.android.systemui.statusbar.KeyguardAffordanceView; 46import com.android.systemui.statusbar.MirrorView; 47import com.android.systemui.statusbar.StatusBarState; 48import com.android.systemui.statusbar.policy.KeyguardUserSwitcher; 49import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 50import com.android.systemui.statusbar.stack.StackStateAnimator; 51 52public class NotificationPanelView extends PanelView implements 53 ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, 54 View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, 55 KeyguardAffordanceHelper.Callback { 56 57 // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is 58 // changed. 59 private static final int CAP_HEIGHT = 1456; 60 private static final int FONT_HEIGHT = 2163; 61 62 private static final float HEADER_RUBBERBAND_FACTOR = 2.15f; 63 private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f; 64 65 private KeyguardAffordanceHelper mAfforanceHelper; 66 private StatusBarHeaderView mHeader; 67 private KeyguardUserSwitcher mKeyguardUserSwitcher; 68 private KeyguardStatusBarView mKeyguardStatusBar; 69 private View mQsContainer; 70 private QSPanel mQsPanel; 71 private View mKeyguardStatusView; 72 private ObservableScrollView mScrollView; 73 private TextView mClockView; 74 private View mReserveNotificationSpace; 75 private MirrorView mSystemIconsCopy; 76 private View mQsNavbarScrim; 77 private View mNotificationContainerParent; 78 private NotificationStackScrollLayout mNotificationStackScroller; 79 private int mNotificationTopPadding; 80 private boolean mAnimateNextTopPaddingChange; 81 82 private int mTrackingPointer; 83 private VelocityTracker mVelocityTracker; 84 private boolean mQsTracking; 85 86 /** 87 * Handles launching the secure camera properly even when other applications may be using the 88 * camera hardware. 89 */ 90 private SecureCameraLaunchManager mSecureCameraLaunchManager; 91 92 /** 93 * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and 94 * the expansion for quick settings. 95 */ 96 private boolean mConflictingQsExpansionGesture; 97 98 /** 99 * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't 100 * intercepted yet. 101 */ 102 private boolean mIntercepting; 103 private boolean mQsExpanded; 104 private boolean mQsExpandedWhenExpandingStarted; 105 private boolean mQsFullyExpanded; 106 private boolean mKeyguardShowing; 107 private boolean mDozing; 108 private int mStatusBarState; 109 private float mInitialHeightOnTouch; 110 private float mInitialTouchX; 111 private float mInitialTouchY; 112 private float mLastTouchX; 113 private float mLastTouchY; 114 private float mQsExpansionHeight; 115 private int mQsMinExpansionHeight; 116 private int mQsMaxExpansionHeight; 117 private int mQsPeekHeight; 118 private boolean mStackScrollerOverscrolling; 119 private boolean mQsExpansionFromOverscroll; 120 private float mLastOverscroll; 121 private boolean mQsExpansionEnabled = true; 122 private ValueAnimator mQsExpansionAnimator; 123 private FlingAnimationUtils mFlingAnimationUtils; 124 private int mStatusBarMinHeight; 125 private boolean mUnlockIconActive; 126 private int mNotificationsHeaderCollideDistance; 127 private int mUnlockMoveDistance; 128 private float mEmptyDragAmount; 129 130 private Interpolator mFastOutSlowInInterpolator; 131 private Interpolator mFastOutLinearInterpolator; 132 private ObjectAnimator mClockAnimator; 133 private int mClockAnimationTarget = -1; 134 private int mTopPaddingAdjustment; 135 private KeyguardClockPositionAlgorithm mClockPositionAlgorithm = 136 new KeyguardClockPositionAlgorithm(); 137 private KeyguardClockPositionAlgorithm.Result mClockPositionResult = 138 new KeyguardClockPositionAlgorithm.Result(); 139 private boolean mIsExpanding; 140 141 private boolean mBlockTouches; 142 private int mNotificationScrimWaitDistance; 143 private boolean mTwoFingerQsExpand; 144 private boolean mTwoFingerQsExpandPossible; 145 146 /** 147 * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still 148 * need to take this into account in our panel height calculation. 149 */ 150 private int mScrollYOverride = -1; 151 private boolean mQsAnimatorExpand; 152 private boolean mIsLaunchTransitionFinished; 153 private boolean mIsLaunchTransitionRunning; 154 private Runnable mLaunchAnimationEndRunnable; 155 private boolean mOnlyAffordanceInThisMotion; 156 private boolean mKeyguardStatusViewAnimating; 157 private boolean mHeaderAnimatingIn; 158 private ObjectAnimator mQsContainerAnimator; 159 160 private boolean mShadeEmpty; 161 162 private boolean mQsScrimEnabled = true; 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() 429 .add(getContext().getString(R.string.accessibility_desc_notification_shade)); 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 mLastOverscroll = 0f; 738 setQsExpansion(mQsExpansionHeight); 739 flingSettings(velocity, open, new Runnable() { 740 @Override 741 public void run() { 742 mStackScrollerOverscrolling = false; 743 mQsExpansionFromOverscroll = false; 744 updateQsState(); 745 } 746 }); 747 } 748 749 private void onQsExpansionStarted() { 750 onQsExpansionStarted(0); 751 } 752 753 private void onQsExpansionStarted(int overscrollAmount) { 754 cancelAnimation(); 755 756 // Reset scroll position and apply that position to the expanded height. 757 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 758 if (mScrollView.getScrollY() != 0) { 759 mScrollYOverride = mScrollView.getScrollY(); 760 } 761 mScrollView.scrollTo(0, 0); 762 setQsExpansion(height); 763 } 764 765 private void setQsExpanded(boolean expanded) { 766 boolean changed = mQsExpanded != expanded; 767 if (changed) { 768 mQsExpanded = expanded; 769 updateQsState(); 770 requestPanelHeightUpdate(); 771 mNotificationStackScroller.setInterceptDelegateEnabled(expanded); 772 mStatusBar.setQsExpanded(expanded); 773 } 774 } 775 776 public void setBarState(int statusBarState, boolean keyguardFadingAway, 777 boolean goingToFullShade) { 778 boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD 779 || statusBarState == StatusBarState.SHADE_LOCKED; 780 if (!mKeyguardShowing && keyguardShowing) { 781 setQsTranslation(mQsExpansionHeight); 782 mHeader.setTranslationY(0f); 783 } 784 setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade); 785 setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); 786 if (goingToFullShade) { 787 animateKeyguardStatusBarOut(); 788 } else { 789 mKeyguardStatusBar.setAlpha(1f); 790 mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE); 791 } 792 mStatusBarState = statusBarState; 793 mKeyguardShowing = keyguardShowing; 794 updateQsState(); 795 if (goingToFullShade) { 796 animateHeaderSlidingIn(); 797 } 798 } 799 800 private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() { 801 @Override 802 public void run() { 803 mKeyguardStatusViewAnimating = false; 804 mKeyguardStatusView.setVisibility(View.GONE); 805 } 806 }; 807 808 private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() { 809 @Override 810 public void run() { 811 mKeyguardStatusViewAnimating = false; 812 } 813 }; 814 815 private final Animator.AnimatorListener mAnimateHeaderSlidingInListener 816 = new AnimatorListenerAdapter() { 817 @Override 818 public void onAnimationEnd(Animator animation) { 819 mHeaderAnimatingIn = false; 820 mQsContainerAnimator = null; 821 mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater); 822 } 823 }; 824 825 private final OnLayoutChangeListener mQsContainerAnimatorUpdater 826 = new OnLayoutChangeListener() { 827 @Override 828 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 829 int oldTop, int oldRight, int oldBottom) { 830 int oldHeight = oldBottom - oldTop; 831 int height = bottom - top; 832 if (height != oldHeight && mQsContainerAnimator != null) { 833 PropertyValuesHolder[] values = mQsContainerAnimator.getValues(); 834 float newEndValue = mHeader.getCollapsedHeight() + mQsPeekHeight - height - top; 835 float newStartValue = -height - top; 836 values[0].setFloatValues(newStartValue, newEndValue); 837 mQsContainerAnimator.setCurrentPlayTime(mQsContainerAnimator.getCurrentPlayTime()); 838 } 839 } 840 }; 841 842 private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn 843 = new ViewTreeObserver.OnPreDrawListener() { 844 @Override 845 public boolean onPreDraw() { 846 getViewTreeObserver().removeOnPreDrawListener(this); 847 mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight); 848 mHeader.animate() 849 .translationY(0f) 850 .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()) 851 .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE) 852 .setInterpolator(mFastOutSlowInInterpolator) 853 .start(); 854 mQsContainer.setY(-mQsContainer.getHeight()); 855 mQsContainerAnimator = ObjectAnimator.ofFloat(mQsContainer, View.TRANSLATION_Y, 856 mQsContainer.getTranslationY(), 857 mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight() 858 - mQsContainer.getTop()); 859 mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay()); 860 mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE); 861 mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator); 862 mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener); 863 mQsContainerAnimator.start(); 864 mQsContainer.addOnLayoutChangeListener(mQsContainerAnimatorUpdater); 865 return true; 866 } 867 }; 868 869 private void animateHeaderSlidingIn() { 870 mHeaderAnimatingIn = true; 871 getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn); 872 873 } 874 875 private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() { 876 @Override 877 public void run() { 878 mKeyguardStatusBar.setVisibility(View.INVISIBLE); 879 } 880 }; 881 882 private void animateKeyguardStatusBarOut() { 883 mKeyguardStatusBar.animate() 884 .alpha(0f) 885 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 886 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 887 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 888 .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable) 889 .start(); 890 } 891 892 private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() { 893 @Override 894 public void run() { 895 mKeyguardBottomArea.setVisibility(View.GONE); 896 } 897 }; 898 899 private void setKeyguardBottomAreaVisibility(int statusBarState, 900 boolean goingToFullShade) { 901 if (goingToFullShade) { 902 mKeyguardBottomArea.animate().cancel(); 903 mKeyguardBottomArea.animate() 904 .alpha(0f) 905 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 906 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 907 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 908 .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable) 909 .start(); 910 } else if (statusBarState == StatusBarState.KEYGUARD 911 || statusBarState == StatusBarState.SHADE_LOCKED) { 912 mKeyguardBottomArea.animate().cancel(); 913 mKeyguardBottomArea.setVisibility(View.VISIBLE); 914 mKeyguardBottomArea.setAlpha(1f); 915 } else { 916 mKeyguardBottomArea.animate().cancel(); 917 mKeyguardBottomArea.setVisibility(View.GONE); 918 mKeyguardBottomArea.setAlpha(1f); 919 } 920 } 921 922 private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway, 923 boolean goingToFullShade) { 924 if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD 925 && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) { 926 mKeyguardStatusView.animate().cancel(); 927 mKeyguardStatusViewAnimating = true; 928 mKeyguardStatusView.animate() 929 .alpha(0f) 930 .setStartDelay(0) 931 .setDuration(160) 932 .setInterpolator(PhoneStatusBar.ALPHA_OUT) 933 .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable); 934 if (keyguardFadingAway) { 935 mKeyguardStatusView.animate() 936 .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay()) 937 .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2) 938 .start(); 939 } 940 } else if (mStatusBarState == StatusBarState.SHADE_LOCKED 941 && statusBarState == StatusBarState.KEYGUARD) { 942 mKeyguardStatusView.animate().cancel(); 943 mKeyguardStatusView.setVisibility(View.VISIBLE); 944 mKeyguardStatusViewAnimating = true; 945 mKeyguardStatusView.setAlpha(0f); 946 mKeyguardStatusView.animate() 947 .alpha(1f) 948 .setStartDelay(0) 949 .setDuration(320) 950 .setInterpolator(PhoneStatusBar.ALPHA_IN) 951 .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable); 952 } else if (statusBarState == StatusBarState.KEYGUARD) { 953 mKeyguardStatusView.animate().cancel(); 954 mKeyguardStatusView.setVisibility(View.VISIBLE); 955 mKeyguardStatusView.setAlpha(1f); 956 } else { 957 mKeyguardStatusView.animate().cancel(); 958 mKeyguardStatusView.setVisibility(View.GONE); 959 mKeyguardStatusView.setAlpha(1f); 960 } 961 } 962 963 private void updateQsState() { 964 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; 965 mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE); 966 mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling)); 967 mNotificationStackScroller.setScrollingEnabled( 968 mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded 969 || mQsExpansionFromOverscroll)); 970 mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 971 mQsContainer.setVisibility( 972 mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE); 973 mScrollView.setTouchEnabled(mQsExpanded); 974 updateEmptyShadeView(); 975 mQsNavbarScrim.setVisibility(mStatusBarState == StatusBarState.SHADE && mQsExpanded 976 && !mStackScrollerOverscrolling && mQsScrimEnabled 977 ? View.VISIBLE 978 : View.INVISIBLE); 979 if (mKeyguardUserSwitcher != null && mQsExpanded && !mStackScrollerOverscrolling) { 980 mKeyguardUserSwitcher.hide(true /* animate */); 981 } 982 } 983 984 private void setQsExpansion(float height) { 985 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); 986 mQsFullyExpanded = height == mQsMaxExpansionHeight; 987 if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) { 988 setQsExpanded(true); 989 } else if (height <= mQsMinExpansionHeight && mQsExpanded) { 990 setQsExpanded(false); 991 } 992 mQsExpansionHeight = height; 993 mHeader.setExpansion(getHeaderExpansionFraction()); 994 setQsTranslation(height); 995 requestScrollerTopPaddingUpdate(false /* animate */); 996 updateNotificationScrim(height); 997 if (mKeyguardShowing) { 998 updateHeaderKeyguard(); 999 } 1000 if (mStatusBarState == StatusBarState.SHADE && mQsExpanded 1001 && !mStackScrollerOverscrolling && mQsScrimEnabled) { 1002 mQsNavbarScrim.setAlpha(getQsExpansionFraction()); 1003 } 1004 } 1005 1006 private void updateNotificationScrim(float height) { 1007 int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance; 1008 float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance); 1009 progress = Math.max(0.0f, Math.min(progress, 1.0f)); 1010 mNotificationStackScroller.setScrimAlpha(progress); 1011 } 1012 1013 private float getHeaderExpansionFraction() { 1014 if (!mKeyguardShowing) { 1015 return getQsExpansionFraction(); 1016 } else { 1017 return 1f; 1018 } 1019 } 1020 1021 private void setQsTranslation(float height) { 1022 if (!mHeaderAnimatingIn) { 1023 mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation()); 1024 } 1025 if (mKeyguardShowing) { 1026 mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0)); 1027 } 1028 } 1029 1030 private float calculateQsTopPadding() { 1031 // We can only do the smoother transition on Keyguard when we also are not collapsing from a 1032 // scrolled quick settings. 1033 if (mKeyguardShowing && mScrollYOverride == -1) { 1034 return interpolate(getQsExpansionFraction(), 1035 mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding, 1036 mQsMaxExpansionHeight); 1037 } else { 1038 return mQsExpansionHeight; 1039 } 1040 } 1041 1042 private void requestScrollerTopPaddingUpdate(boolean animate) { 1043 mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(), 1044 mScrollView.getScrollY(), 1045 mAnimateNextTopPaddingChange || animate); 1046 mAnimateNextTopPaddingChange = false; 1047 } 1048 1049 private void trackMovement(MotionEvent event) { 1050 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 1051 mLastTouchX = event.getX(); 1052 mLastTouchY = event.getY(); 1053 } 1054 1055 private void initVelocityTracker() { 1056 if (mVelocityTracker != null) { 1057 mVelocityTracker.recycle(); 1058 } 1059 mVelocityTracker = VelocityTracker.obtain(); 1060 } 1061 1062 private float getCurrentVelocity() { 1063 if (mVelocityTracker == null) { 1064 return 0; 1065 } 1066 mVelocityTracker.computeCurrentVelocity(1000); 1067 return mVelocityTracker.getYVelocity(); 1068 } 1069 1070 private void cancelAnimation() { 1071 if (mQsExpansionAnimator != null) { 1072 mQsExpansionAnimator.cancel(); 1073 } 1074 } 1075 1076 private void flingSettings(float vel, boolean expand) { 1077 flingSettings(vel, expand, null); 1078 } 1079 1080 private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) { 1081 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 1082 if (target == mQsExpansionHeight) { 1083 mScrollYOverride = -1; 1084 if (onFinishRunnable != null) { 1085 onFinishRunnable.run(); 1086 } 1087 return; 1088 } 1089 mScrollView.setBlockFlinging(true); 1090 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 1091 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 1092 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1093 @Override 1094 public void onAnimationUpdate(ValueAnimator animation) { 1095 setQsExpansion((Float) animation.getAnimatedValue()); 1096 } 1097 }); 1098 animator.addListener(new AnimatorListenerAdapter() { 1099 @Override 1100 public void onAnimationEnd(Animator animation) { 1101 mScrollView.setBlockFlinging(false); 1102 mScrollYOverride = -1; 1103 mQsExpansionAnimator = null; 1104 if (onFinishRunnable != null) { 1105 onFinishRunnable.run(); 1106 } 1107 } 1108 }); 1109 animator.start(); 1110 mQsExpansionAnimator = animator; 1111 mQsAnimatorExpand = expand; 1112 } 1113 1114 /** 1115 * @return Whether we should intercept a gesture to open Quick Settings. 1116 */ 1117 private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { 1118 if (!mQsExpansionEnabled) { 1119 return false; 1120 } 1121 View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader; 1122 boolean onHeader = x >= header.getLeft() && x <= header.getRight() 1123 && y >= header.getTop() && y <= header.getBottom(); 1124 if (mQsExpanded) { 1125 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0) && isInQsArea(x, y); 1126 } else { 1127 return onHeader; 1128 } 1129 } 1130 1131 @Override 1132 public void setVisibility(int visibility) { 1133 int oldVisibility = getVisibility(); 1134 super.setVisibility(visibility); 1135 if (visibility != oldVisibility) { 1136 reparentStatusIcons(visibility == VISIBLE); 1137 } 1138 } 1139 1140 /** 1141 * When the notification panel gets expanded, we need to move the status icons in the header 1142 * card. 1143 */ 1144 private void reparentStatusIcons(boolean toHeader) { 1145 if (mStatusBar == null) { 1146 return; 1147 } 1148 LinearLayout systemIcons = mStatusBar.getSystemIcons(); 1149 ViewGroup parent = ((ViewGroup) systemIcons.getParent()); 1150 if (toHeader) { 1151 int index = parent.indexOfChild(systemIcons); 1152 parent.removeView(systemIcons); 1153 mSystemIconsCopy.setMirroredView( 1154 systemIcons, systemIcons.getWidth(), systemIcons.getHeight()); 1155 parent.addView(mSystemIconsCopy, index); 1156 mHeader.attachSystemIcons(systemIcons); 1157 } else { 1158 ViewGroup newParent = mStatusBar.getSystemIconArea(); 1159 int index = newParent.indexOfChild(mSystemIconsCopy); 1160 parent.removeView(systemIcons); 1161 mHeader.onSystemIconsDetached(); 1162 mSystemIconsCopy.setMirroredView(null, 0, 0); 1163 newParent.removeView(mSystemIconsCopy); 1164 newParent.addView(systemIcons, index); 1165 } 1166 } 1167 1168 @Override 1169 protected boolean isScrolledToBottom() { 1170 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 1171 return true; 1172 } 1173 if (!isInSettings()) { 1174 return mNotificationStackScroller.isScrolledToBottom(); 1175 } else { 1176 return mScrollView.isScrolledToBottom(); 1177 } 1178 } 1179 1180 @Override 1181 protected int getMaxPanelHeight() { 1182 int min = mStatusBarMinHeight; 1183 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD 1184 && mNotificationStackScroller.getNotGoneChildCount() == 0) { 1185 int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount()) 1186 * HEADER_RUBBERBAND_FACTOR); 1187 min = Math.max(min, minHeight); 1188 } 1189 int maxHeight; 1190 if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) { 1191 maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade()); 1192 } else { 1193 maxHeight = calculatePanelHeightShade(); 1194 } 1195 maxHeight = Math.max(maxHeight, min); 1196 return maxHeight; 1197 } 1198 1199 private boolean isInSettings() { 1200 return mQsExpanded; 1201 } 1202 1203 @Override 1204 protected void onHeightUpdated(float expandedHeight) { 1205 if (!mQsExpanded) { 1206 positionClockAndNotifications(); 1207 } 1208 if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null 1209 && !mQsExpansionFromOverscroll) { 1210 float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding() 1211 + mNotificationStackScroller.getMinStackHeight() 1212 + mNotificationStackScroller.getNotificationTopPadding(); 1213 float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); 1214 float t = (expandedHeight - panelHeightQsCollapsed) 1215 / (panelHeightQsExpanded - panelHeightQsCollapsed); 1216 1217 setQsExpansion(mQsMinExpansionHeight 1218 + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight)); 1219 } 1220 mNotificationStackScroller.setStackHeight(expandedHeight); 1221 updateHeader(); 1222 updateUnlockIcon(); 1223 updateNotificationTranslucency(); 1224 } 1225 1226 /** 1227 * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when 1228 * collapsing QS / the panel when QS was scrolled 1229 */ 1230 private int getTempQsMaxExpansion() { 1231 int qsTempMaxExpansion = mQsMaxExpansionHeight; 1232 if (mScrollYOverride != -1) { 1233 qsTempMaxExpansion -= mScrollYOverride; 1234 } 1235 return qsTempMaxExpansion; 1236 } 1237 1238 private int calculatePanelHeightShade() { 1239 int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin(); 1240 int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin 1241 - mTopPaddingAdjustment; 1242 maxHeight += mNotificationStackScroller.getTopPaddingOverflow(); 1243 return maxHeight; 1244 } 1245 1246 private int calculatePanelHeightQsExpanded() { 1247 float notificationHeight = mNotificationStackScroller.getHeight() 1248 - mNotificationStackScroller.getEmptyBottomMargin() 1249 - mNotificationStackScroller.getTopPadding(); 1250 float totalHeight = mQsMaxExpansionHeight + notificationHeight 1251 + mNotificationStackScroller.getNotificationTopPadding(); 1252 if (totalHeight > mNotificationStackScroller.getHeight()) { 1253 float fullyCollapsedHeight = mQsMaxExpansionHeight 1254 + mNotificationStackScroller.getMinStackHeight() 1255 + mNotificationStackScroller.getNotificationTopPadding() 1256 - getScrollViewScrollY(); 1257 totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight()); 1258 } 1259 return (int) totalHeight; 1260 } 1261 1262 private int getScrollViewScrollY() { 1263 if (mScrollYOverride != -1) { 1264 return mScrollYOverride; 1265 } else { 1266 return mScrollView.getScrollY(); 1267 } 1268 } 1269 private void updateNotificationTranslucency() { 1270 float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight()) 1271 / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize() 1272 + mNotificationStackScroller.getCollapseSecondCardPadding()); 1273 alpha = Math.max(0, Math.min(alpha, 1)); 1274 alpha = (float) Math.pow(alpha, 0.75); 1275 if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) { 1276 mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null); 1277 } else if (alpha == 1f 1278 && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) { 1279 mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null); 1280 } 1281 mNotificationStackScroller.setAlpha(alpha); 1282 } 1283 1284 @Override 1285 protected float getOverExpansionAmount() { 1286 return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */); 1287 } 1288 1289 @Override 1290 protected float getOverExpansionPixels() { 1291 return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */); 1292 } 1293 1294 private void updateUnlockIcon() { 1295 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1296 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1297 boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance; 1298 KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon(); 1299 if (active && !mUnlockIconActive && mTracking) { 1300 lockIcon.setImageAlpha(1.0f, true, 150, mFastOutLinearInterpolator, null); 1301 lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150, 1302 mFastOutLinearInterpolator); 1303 } else if (!active && mUnlockIconActive && mTracking) { 1304 lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true, 1305 150, mFastOutLinearInterpolator, null); 1306 lockIcon.setImageScale(1.0f, true, 150, 1307 mFastOutLinearInterpolator); 1308 } 1309 mUnlockIconActive = active; 1310 } 1311 } 1312 1313 /** 1314 * Hides the header when notifications are colliding with it. 1315 */ 1316 private void updateHeader() { 1317 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1318 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1319 updateHeaderKeyguard(); 1320 } else { 1321 updateHeaderShade(); 1322 } 1323 1324 } 1325 1326 private void updateHeaderShade() { 1327 if (!mHeaderAnimatingIn) { 1328 mHeader.setTranslationY(getHeaderTranslation()); 1329 } 1330 setQsTranslation(mQsExpansionHeight); 1331 } 1332 1333 private float getHeaderTranslation() { 1334 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1335 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1336 return 0; 1337 } 1338 if (mNotificationStackScroller.getNotGoneChildCount() == 0) { 1339 if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) { 1340 return 0; 1341 } else { 1342 return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight; 1343 } 1344 } 1345 return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR; 1346 } 1347 1348 private void updateHeaderKeyguard() { 1349 float alphaNotifications; 1350 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 1351 1352 // When on Keyguard, we hide the header as soon as the top card of the notification 1353 // stack scroller is close enough (collision distance) to the bottom of the header. 1354 alphaNotifications = getNotificationsTopY() 1355 / 1356 (mKeyguardStatusBar.getHeight() + mNotificationsHeaderCollideDistance); 1357 } else { 1358 1359 // In SHADE_LOCKED, the top card is already really close to the header. Hide it as 1360 // soon as we start translating the stack. 1361 alphaNotifications = getNotificationsTopY() / mKeyguardStatusBar.getHeight(); 1362 } 1363 alphaNotifications = MathUtils.constrain(alphaNotifications, 0, 1); 1364 alphaNotifications = (float) Math.pow(alphaNotifications, 0.75); 1365 float alphaQsExpansion = 1 - Math.min(1, getQsExpansionFraction() * 2); 1366 mKeyguardStatusBar.setAlpha(Math.min(alphaNotifications, alphaQsExpansion)); 1367 mKeyguardBottomArea.setAlpha(Math.min(1 - getQsExpansionFraction(), alphaNotifications)); 1368 setQsTranslation(mQsExpansionHeight); 1369 } 1370 1371 private float getNotificationsTopY() { 1372 if (mNotificationStackScroller.getNotGoneChildCount() == 0) { 1373 return getExpandedHeight(); 1374 } 1375 return mNotificationStackScroller.getNotificationsTopY(); 1376 } 1377 1378 @Override 1379 protected void onExpandingStarted() { 1380 super.onExpandingStarted(); 1381 mNotificationStackScroller.onExpansionStarted(); 1382 mIsExpanding = true; 1383 mQsExpandedWhenExpandingStarted = mQsExpanded; 1384 if (mQsExpanded) { 1385 onQsExpansionStarted(); 1386 } 1387 } 1388 1389 @Override 1390 protected void onExpandingFinished() { 1391 super.onExpandingFinished(); 1392 mNotificationStackScroller.onExpansionStopped(); 1393 mIsExpanding = false; 1394 mScrollYOverride = -1; 1395 if (mExpandedHeight == 0f) { 1396 setListening(false); 1397 } else { 1398 setListening(true); 1399 } 1400 mTwoFingerQsExpand = false; 1401 mTwoFingerQsExpandPossible = false; 1402 } 1403 1404 private void setListening(boolean listening) { 1405 mHeader.setListening(listening); 1406 mKeyguardStatusBar.setListening(listening); 1407 mQsPanel.setListening(listening); 1408 } 1409 1410 @Override 1411 public void instantExpand() { 1412 super.instantExpand(); 1413 setListening(true); 1414 } 1415 1416 @Override 1417 protected void setOverExpansion(float overExpansion, boolean isPixels) { 1418 if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) { 1419 return; 1420 } 1421 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 1422 mNotificationStackScroller.setOnHeightChangedListener(null); 1423 if (isPixels) { 1424 mNotificationStackScroller.setOverScrolledPixels( 1425 overExpansion, true /* onTop */, false /* animate */); 1426 } else { 1427 mNotificationStackScroller.setOverScrollAmount( 1428 overExpansion, true /* onTop */, false /* animate */); 1429 } 1430 mNotificationStackScroller.setOnHeightChangedListener(this); 1431 } 1432 } 1433 1434 @Override 1435 protected void onTrackingStarted() { 1436 super.onTrackingStarted(); 1437 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1438 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1439 mAfforanceHelper.animateHideLeftRightIcon(); 1440 } 1441 if (mQsExpanded) { 1442 mTwoFingerQsExpand = true; 1443 } 1444 } 1445 1446 @Override 1447 protected void onTrackingStopped(boolean expand) { 1448 super.onTrackingStopped(expand); 1449 if (expand) { 1450 mNotificationStackScroller.setOverScrolledPixels( 1451 0.0f, true /* onTop */, true /* animate */); 1452 } 1453 if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1454 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1455 if (!mHintAnimationRunning) { 1456 mAfforanceHelper.reset(true); 1457 } 1458 } 1459 if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1460 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1461 KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon(); 1462 lockIcon.setImageAlpha(0.0f, true, 100, mFastOutLinearInterpolator, null); 1463 lockIcon.setImageScale(2.0f, true, 100, mFastOutLinearInterpolator); 1464 } 1465 } 1466 1467 @Override 1468 public void onHeightChanged(ExpandableView view) { 1469 1470 // Block update if we are in quick settings and just the top padding changed 1471 // (i.e. view == null). 1472 if (view == null && mQsExpanded) { 1473 return; 1474 } 1475 requestPanelHeightUpdate(); 1476 } 1477 1478 @Override 1479 public void onReset(ExpandableView view) { 1480 } 1481 1482 @Override 1483 public void onScrollChanged() { 1484 if (mQsExpanded) { 1485 requestScrollerTopPaddingUpdate(false /* animate */); 1486 requestPanelHeightUpdate(); 1487 } 1488 } 1489 1490 @Override 1491 protected void onConfigurationChanged(Configuration newConfig) { 1492 super.onConfigurationChanged(newConfig); 1493 mAfforanceHelper.onConfigurationChanged(); 1494 } 1495 1496 @Override 1497 public void onClick(View v) { 1498 if (v == mHeader) { 1499 onQsExpansionStarted(); 1500 if (mQsExpanded) { 1501 flingSettings(0 /* vel */, false /* expand */); 1502 } else if (mQsExpansionEnabled) { 1503 flingSettings(0 /* vel */, true /* expand */); 1504 } 1505 } 1506 } 1507 1508 @Override 1509 public void onAnimationToSideStarted(boolean rightPage) { 1510 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; 1511 mIsLaunchTransitionRunning = true; 1512 mLaunchAnimationEndRunnable = null; 1513 if (start) { 1514 mKeyguardBottomArea.launchPhone(); 1515 } else { 1516 mSecureCameraLaunchManager.startSecureCameraLaunch(); 1517 } 1518 mBlockTouches = true; 1519 } 1520 1521 @Override 1522 public void onAnimationToSideEnded() { 1523 mIsLaunchTransitionRunning = false; 1524 mIsLaunchTransitionFinished = true; 1525 if (mLaunchAnimationEndRunnable != null) { 1526 mLaunchAnimationEndRunnable.run(); 1527 mLaunchAnimationEndRunnable = null; 1528 } 1529 } 1530 1531 @Override 1532 protected void onEdgeClicked(boolean right) { 1533 if ((right && getRightIcon().getVisibility() != View.VISIBLE) 1534 || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) { 1535 return; 1536 } 1537 mHintAnimationRunning = true; 1538 mAfforanceHelper.startHintAnimation(right, new Runnable() { 1539 @Override 1540 public void run() { 1541 mHintAnimationRunning = false; 1542 mStatusBar.onHintFinished(); 1543 } 1544 }); 1545 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; 1546 if (start) { 1547 mStatusBar.onPhoneHintStarted(); 1548 } else { 1549 mStatusBar.onCameraHintStarted(); 1550 } 1551 } 1552 1553 @Override 1554 protected void startUnlockHintAnimation() { 1555 super.startUnlockHintAnimation(); 1556 startHighlightIconAnimation(getCenterIcon()); 1557 } 1558 1559 /** 1560 * Starts the highlight (making it fully opaque) animation on an icon. 1561 */ 1562 private void startHighlightIconAnimation(final KeyguardAffordanceView icon) { 1563 icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, 1564 mFastOutSlowInInterpolator, new Runnable() { 1565 @Override 1566 public void run() { 1567 icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, 1568 true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION, 1569 mFastOutSlowInInterpolator, null); 1570 } 1571 }); 1572 } 1573 1574 @Override 1575 public float getPageWidth() { 1576 return getWidth(); 1577 } 1578 1579 @Override 1580 public void onSwipingStarted() { 1581 mSecureCameraLaunchManager.onSwipingStarted(); 1582 requestDisallowInterceptTouchEvent(true); 1583 mOnlyAffordanceInThisMotion = true; 1584 } 1585 1586 @Override 1587 public KeyguardAffordanceView getLeftIcon() { 1588 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1589 ? mKeyguardBottomArea.getCameraView() 1590 : mKeyguardBottomArea.getPhoneView(); 1591 } 1592 1593 @Override 1594 public KeyguardAffordanceView getCenterIcon() { 1595 return mKeyguardBottomArea.getLockIcon(); 1596 } 1597 1598 @Override 1599 public KeyguardAffordanceView getRightIcon() { 1600 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1601 ? mKeyguardBottomArea.getPhoneView() 1602 : mKeyguardBottomArea.getCameraView(); 1603 } 1604 1605 @Override 1606 public View getLeftPreview() { 1607 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1608 ? mKeyguardBottomArea.getCameraPreview() 1609 : mKeyguardBottomArea.getPhonePreview(); 1610 } 1611 1612 @Override 1613 public View getRightPreview() { 1614 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1615 ? mKeyguardBottomArea.getPhonePreview() 1616 : mKeyguardBottomArea.getCameraPreview(); 1617 } 1618 1619 @Override 1620 protected float getPeekHeight() { 1621 if (mNotificationStackScroller.getNotGoneChildCount() > 0) { 1622 return mNotificationStackScroller.getPeekHeight(); 1623 } else { 1624 return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR; 1625 } 1626 } 1627 1628 @Override 1629 protected float getCannedFlingDurationFactor() { 1630 if (mQsExpanded) { 1631 return 0.7f; 1632 } else { 1633 return 0.6f; 1634 } 1635 } 1636 1637 @Override 1638 protected boolean fullyExpandedClearAllVisible() { 1639 return mNotificationStackScroller.isDismissViewNotGone() 1640 && mNotificationStackScroller.isScrolledToBottom(); 1641 } 1642 1643 @Override 1644 protected boolean isClearAllVisible() { 1645 return mNotificationStackScroller.isDismissViewVisible(); 1646 } 1647 1648 @Override 1649 protected int getClearAllHeight() { 1650 return mNotificationStackScroller.getDismissViewHeight(); 1651 } 1652 1653 @Override 1654 protected boolean isTrackingBlocked() { 1655 return mConflictingQsExpansionGesture && mQsExpanded; 1656 } 1657 1658 public void notifyVisibleChildrenChanged() { 1659 if (mNotificationStackScroller.getNotGoneChildCount() != 0) { 1660 mReserveNotificationSpace.setVisibility(View.VISIBLE); 1661 } else { 1662 mReserveNotificationSpace.setVisibility(View.GONE); 1663 } 1664 } 1665 1666 public boolean isQsExpanded() { 1667 return mQsExpanded; 1668 } 1669 1670 public boolean isQsDetailShowing() { 1671 return mQsPanel.isShowingDetail(); 1672 } 1673 1674 public void closeQsDetail() { 1675 mQsPanel.closeDetail(); 1676 } 1677 1678 @Override 1679 public boolean shouldDelayChildPressedState() { 1680 return true; 1681 } 1682 1683 public boolean isLaunchTransitionFinished() { 1684 return mIsLaunchTransitionFinished; 1685 } 1686 1687 public boolean isLaunchTransitionRunning() { 1688 return mIsLaunchTransitionRunning; 1689 } 1690 1691 public void setLaunchTransitionEndRunnable(Runnable r) { 1692 mLaunchAnimationEndRunnable = r; 1693 } 1694 1695 public void setEmptyDragAmount(float amount) { 1696 float factor = 1f; 1697 if (mNotificationStackScroller.getNotGoneChildCount() > 0) { 1698 factor = 0.6f; 1699 } else if (!mStatusBar.hasActiveNotifications()) { 1700 factor = 0.4f; 1701 } 1702 mEmptyDragAmount = amount * factor; 1703 positionClockAndNotifications(); 1704 } 1705 1706 private static float interpolate(float t, float start, float end) { 1707 return (1 - t) * start + t * end; 1708 } 1709 1710 private void updateKeyguardStatusBarVisibility() { 1711 mKeyguardStatusBar.setVisibility(mKeyguardShowing && !mDozing ? VISIBLE : INVISIBLE); 1712 } 1713 1714 public void setDozing(boolean dozing) { 1715 if (dozing == mDozing) return; 1716 mDozing = dozing; 1717 if (mDozing) { 1718 setBackgroundColor(0xff000000); 1719 } else { 1720 setBackground(null); 1721 } 1722 updateKeyguardStatusBarVisibility(); 1723 } 1724 1725 public void setShadeEmpty(boolean shadeEmpty) { 1726 mShadeEmpty = shadeEmpty; 1727 updateEmptyShadeView(); 1728 } 1729 1730 private void updateEmptyShadeView() { 1731 1732 // Hide "No notifications" in QS. 1733 mNotificationStackScroller.updateEmptyShadeView(mShadeEmpty && !mQsExpanded); 1734 } 1735 1736 public void setQsScrimEnabled(boolean qsScrimEnabled) { 1737 boolean changed = mQsScrimEnabled != qsScrimEnabled; 1738 mQsScrimEnabled = qsScrimEnabled; 1739 if (changed) { 1740 updateQsState(); 1741 } 1742 } 1743 1744 public void setKeyguardUserSwitcher(KeyguardUserSwitcher keyguardUserSwitcher) { 1745 mKeyguardUserSwitcher = keyguardUserSwitcher; 1746 } 1747 1748 private final Runnable mUpdateHeader = new Runnable() { 1749 @Override 1750 public void run() { 1751 mHeader.updateEverything(); 1752 } 1753 }; 1754} 1755