NotificationPanelView.java revision b572e4c77711a6a92b244f1b9edbf13710481184
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.ValueAnimator; 23import android.content.Context; 24import android.content.res.Configuration; 25import android.util.AttributeSet; 26import android.view.MotionEvent; 27import android.view.VelocityTracker; 28import android.view.View; 29import android.view.ViewGroup; 30import android.view.ViewTreeObserver; 31import android.view.accessibility.AccessibilityEvent; 32import android.view.animation.AnimationUtils; 33import android.view.animation.Interpolator; 34import android.widget.LinearLayout; 35import android.widget.TextView; 36 37import com.android.systemui.R; 38import com.android.systemui.qs.QSPanel; 39import com.android.systemui.statusbar.ExpandableView; 40import com.android.systemui.statusbar.FlingAnimationUtils; 41import com.android.systemui.statusbar.GestureRecorder; 42import com.android.systemui.statusbar.MirrorView; 43import com.android.systemui.statusbar.StatusBarState; 44import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 45import com.android.systemui.statusbar.stack.StackStateAnimator; 46 47import java.util.ArrayList; 48 49public class NotificationPanelView extends PanelView implements 50 ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, 51 View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, 52 KeyguardPageSwipeHelper.Callback { 53 54 // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is 55 // changed. 56 private static final int CAP_HEIGHT = 1456; 57 private static final int FONT_HEIGHT = 2163; 58 59 private static final float HEADER_RUBBERBAND_FACTOR = 2.15f; 60 private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f; 61 62 private KeyguardPageSwipeHelper mPageSwiper; 63 private StatusBarHeaderView mHeader; 64 private View mQsContainer; 65 private QSPanel mQsPanel; 66 private View mKeyguardStatusView; 67 private ObservableScrollView mScrollView; 68 private TextView mClockView; 69 private View mReserveNotificationSpace; 70 private MirrorView mSystemIconsCopy; 71 72 private NotificationStackScrollLayout mNotificationStackScroller; 73 private int mNotificationTopPadding; 74 private boolean mAnimateNextTopPaddingChange; 75 76 private int mTrackingPointer; 77 private VelocityTracker mVelocityTracker; 78 private boolean mQsTracking; 79 80 /** 81 * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and 82 * the expansion for quick settings. 83 */ 84 private boolean mConflictingQsExpansionGesture; 85 86 /** 87 * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't 88 * intercepted yet. 89 */ 90 private boolean mIntercepting; 91 private boolean mQsExpanded; 92 private boolean mQsExpandedWhenExpandingStarted; 93 private boolean mQsFullyExpanded; 94 private boolean mKeyguardShowing; 95 private int mStatusBarState; 96 private float mInitialHeightOnTouch; 97 private float mInitialTouchX; 98 private float mInitialTouchY; 99 private float mLastTouchX; 100 private float mLastTouchY; 101 private float mQsExpansionHeight; 102 private int mQsMinExpansionHeight; 103 private int mQsMaxExpansionHeight; 104 private int mQsPeekHeight; 105 private boolean mStackScrollerOverscrolling; 106 private boolean mQsExpansionFromOverscroll; 107 private boolean mQsExpansionEnabled = true; 108 private ValueAnimator mQsExpansionAnimator; 109 private FlingAnimationUtils mFlingAnimationUtils; 110 private int mStatusBarMinHeight; 111 private boolean mUnlockIconActive; 112 private int mNotificationsHeaderCollideDistance; 113 private int mUnlockMoveDistance; 114 115 private Interpolator mFastOutSlowInInterpolator; 116 private Interpolator mFastOutLinearInterpolator; 117 private ObjectAnimator mClockAnimator; 118 private int mClockAnimationTarget = -1; 119 private int mTopPaddingAdjustment; 120 private KeyguardClockPositionAlgorithm mClockPositionAlgorithm = 121 new KeyguardClockPositionAlgorithm(); 122 private KeyguardClockPositionAlgorithm.Result mClockPositionResult = 123 new KeyguardClockPositionAlgorithm.Result(); 124 private boolean mIsExpanding; 125 126 private boolean mBlockTouches; 127 private ArrayList<View> mSwipeTranslationViews = new ArrayList<>(); 128 private int mNotificationScrimWaitDistance; 129 130 /** 131 * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still 132 * need to take this into account in our panel height calculation. 133 */ 134 private int mScrollYOverride = -1; 135 private boolean mQsAnimatorExpand; 136 137 public NotificationPanelView(Context context, AttributeSet attrs) { 138 super(context, attrs); 139 mSystemIconsCopy = new MirrorView(context); 140 } 141 142 public void setStatusBar(PhoneStatusBar bar) { 143 mStatusBar = bar; 144 } 145 146 @Override 147 protected void onFinishInflate() { 148 super.onFinishInflate(); 149 mHeader = (StatusBarHeaderView) findViewById(R.id.header); 150 mHeader.getBackgroundView().setOnClickListener(this); 151 mHeader.setOverlayParent(this); 152 mKeyguardStatusView = findViewById(R.id.keyguard_status_view); 153 mQsContainer = findViewById(R.id.quick_settings_container); 154 mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel); 155 mClockView = (TextView) findViewById(R.id.clock_view); 156 mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view); 157 mScrollView.setListener(this); 158 mReserveNotificationSpace = findViewById(R.id.reserve_notification_space); 159 mNotificationStackScroller = (NotificationStackScrollLayout) 160 findViewById(R.id.notification_stack_scroller); 161 mNotificationStackScroller.setOnHeightChangedListener(this); 162 mNotificationStackScroller.setOverscrollTopChangedListener(this); 163 mNotificationStackScroller.setScrollView(mScrollView); 164 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), 165 android.R.interpolator.fast_out_slow_in); 166 mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(), 167 android.R.interpolator.fast_out_linear_in); 168 mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area); 169 mSwipeTranslationViews.add(mNotificationStackScroller); 170 mSwipeTranslationViews.add(mKeyguardStatusView); 171 mPageSwiper = new KeyguardPageSwipeHelper(this, getContext()); 172 } 173 174 @Override 175 protected void loadDimens() { 176 super.loadDimens(); 177 mNotificationTopPadding = getResources().getDimensionPixelSize( 178 R.dimen.notifications_top_padding); 179 mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f); 180 mStatusBarMinHeight = getResources().getDimensionPixelSize( 181 com.android.internal.R.dimen.status_bar_height); 182 mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height); 183 mNotificationsHeaderCollideDistance = 184 getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance); 185 mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance); 186 mClockPositionAlgorithm.loadDimens(getResources()); 187 mNotificationScrimWaitDistance = 188 getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance); 189 } 190 191 @Override 192 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 193 super.onLayout(changed, left, top, right, bottom); 194 195 // Update Clock Pivot 196 mKeyguardStatusView.setPivotX(getWidth() / 2); 197 mKeyguardStatusView.setPivotY( 198 (FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize()); 199 200 // Calculate quick setting heights. 201 mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight; 202 mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight(); 203 if (mQsExpanded) { 204 if (mQsFullyExpanded) { 205 mQsExpansionHeight = mQsMaxExpansionHeight; 206 requestScrollerTopPaddingUpdate(false /* animate */); 207 } 208 } else { 209 if (!mStackScrollerOverscrolling) { 210 setQsExpansion(mQsMinExpansionHeight); 211 } 212 positionClockAndNotifications(); 213 mNotificationStackScroller.setStackHeight(getExpandedHeight()); 214 } 215 } 216 217 /** 218 * Positions the clock and notifications dynamically depending on how many notifications are 219 * showing. 220 */ 221 private void positionClockAndNotifications() { 222 boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending(); 223 int stackScrollerPadding; 224 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 225 int bottom = mHeader.getCollapsedHeight(); 226 stackScrollerPadding = bottom + mQsPeekHeight 227 + mNotificationTopPadding; 228 mTopPaddingAdjustment = 0; 229 } else { 230 mClockPositionAlgorithm.setup( 231 mStatusBar.getMaxKeyguardNotifications(), 232 getMaxPanelHeight(), 233 getExpandedHeight(), 234 mNotificationStackScroller.getNotGoneChildCount(), 235 getHeight(), 236 mKeyguardStatusView.getHeight()); 237 mClockPositionAlgorithm.run(mClockPositionResult); 238 if (animate || mClockAnimator != null) { 239 startClockAnimation(mClockPositionResult.clockY); 240 } else { 241 mKeyguardStatusView.setY(mClockPositionResult.clockY); 242 } 243 updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale); 244 stackScrollerPadding = mClockPositionResult.stackScrollerPadding; 245 mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment; 246 } 247 mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding); 248 requestScrollerTopPaddingUpdate(animate); 249 } 250 251 private void startClockAnimation(int y) { 252 if (mClockAnimationTarget == y) { 253 return; 254 } 255 mClockAnimationTarget = y; 256 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 257 @Override 258 public boolean onPreDraw() { 259 getViewTreeObserver().removeOnPreDrawListener(this); 260 if (mClockAnimator != null) { 261 mClockAnimator.removeAllListeners(); 262 mClockAnimator.cancel(); 263 } 264 mClockAnimator = ObjectAnimator 265 .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget); 266 mClockAnimator.setInterpolator(mFastOutSlowInInterpolator); 267 mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 268 mClockAnimator.addListener(new AnimatorListenerAdapter() { 269 @Override 270 public void onAnimationEnd(Animator animation) { 271 mClockAnimator = null; 272 mClockAnimationTarget = -1; 273 } 274 }); 275 mClockAnimator.start(); 276 return true; 277 } 278 }); 279 } 280 281 private void updateClock(float alpha, float scale) { 282 mKeyguardStatusView.setAlpha(alpha); 283 mKeyguardStatusView.setScaleX(scale); 284 mKeyguardStatusView.setScaleY(scale); 285 } 286 287 public void animateToFullShade() { 288 mAnimateNextTopPaddingChange = true; 289 mNotificationStackScroller.goToFullShade(); 290 requestLayout(); 291 } 292 293 public void setQsExpansionEnabled(boolean qsExpansionEnabled) { 294 mQsExpansionEnabled = qsExpansionEnabled; 295 } 296 297 @Override 298 public void resetViews() { 299 mBlockTouches = false; 300 mUnlockIconActive = false; 301 mPageSwiper.reset(); 302 closeQs(); 303 mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */, 304 true /* cancelAnimators */); 305 } 306 307 public void closeQs() { 308 cancelAnimation(); 309 setQsExpansion(mQsMinExpansionHeight); 310 } 311 312 public void animateCloseQs() { 313 if (mQsExpansionAnimator != null) { 314 if (!mQsAnimatorExpand) { 315 return; 316 } 317 float height = mQsExpansionHeight; 318 mQsExpansionAnimator.cancel(); 319 setQsExpansion(height); 320 } 321 flingSettings(0 /* vel */, false); 322 } 323 324 public void openQs() { 325 cancelAnimation(); 326 if (mQsExpansionEnabled) { 327 setQsExpansion(mQsMaxExpansionHeight); 328 } 329 } 330 331 @Override 332 public void fling(float vel, boolean always) { 333 GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); 334 if (gr != null) { 335 gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); 336 } 337 super.fling(vel, always); 338 } 339 340 @Override 341 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 342 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 343 event.getText() 344 .add(getContext().getString(R.string.accessibility_desc_notification_shade)); 345 return true; 346 } 347 348 return super.dispatchPopulateAccessibilityEvent(event); 349 } 350 351 @Override 352 public boolean onInterceptTouchEvent(MotionEvent event) { 353 if (mBlockTouches) { 354 return false; 355 } 356 int pointerIndex = event.findPointerIndex(mTrackingPointer); 357 if (pointerIndex < 0) { 358 pointerIndex = 0; 359 mTrackingPointer = event.getPointerId(pointerIndex); 360 } 361 final float x = event.getX(pointerIndex); 362 final float y = event.getY(pointerIndex); 363 364 switch (event.getActionMasked()) { 365 case MotionEvent.ACTION_DOWN: 366 mIntercepting = true; 367 mInitialTouchY = y; 368 mInitialTouchX = x; 369 initVelocityTracker(); 370 trackMovement(event); 371 if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { 372 getParent().requestDisallowInterceptTouchEvent(true); 373 } 374 if (mQsExpansionAnimator != null) { 375 onQsExpansionStarted(); 376 mInitialHeightOnTouch = mQsExpansionHeight; 377 mQsTracking = true; 378 mIntercepting = false; 379 mNotificationStackScroller.removeLongPressCallback(); 380 } 381 break; 382 case MotionEvent.ACTION_POINTER_UP: 383 final int upPointer = event.getPointerId(event.getActionIndex()); 384 if (mTrackingPointer == upPointer) { 385 // gesture is ongoing, find a new pointer to track 386 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 387 mTrackingPointer = event.getPointerId(newIndex); 388 mInitialTouchX = event.getX(newIndex); 389 mInitialTouchY = event.getY(newIndex); 390 } 391 break; 392 393 case MotionEvent.ACTION_MOVE: 394 final float h = y - mInitialTouchY; 395 trackMovement(event); 396 if (mQsTracking) { 397 398 // Already tracking because onOverscrolled was called. We need to update here 399 // so we don't stop for a frame until the next touch event gets handled in 400 // onTouchEvent. 401 setQsExpansion(h + mInitialHeightOnTouch); 402 trackMovement(event); 403 mIntercepting = false; 404 return true; 405 } 406 if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX) 407 && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { 408 onQsExpansionStarted(); 409 mInitialHeightOnTouch = mQsExpansionHeight; 410 mInitialTouchY = y; 411 mInitialTouchX = x; 412 mQsTracking = true; 413 mIntercepting = false; 414 mNotificationStackScroller.removeLongPressCallback(); 415 return true; 416 } 417 break; 418 419 case MotionEvent.ACTION_CANCEL: 420 case MotionEvent.ACTION_UP: 421 trackMovement(event); 422 if (mQsTracking) { 423 flingQsWithCurrentVelocity(); 424 mQsTracking = false; 425 } 426 mIntercepting = false; 427 break; 428 } 429 return !mQsExpanded && super.onInterceptTouchEvent(event); 430 } 431 432 @Override 433 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 434 435 // Block request when interacting with the scroll view so we can still intercept the 436 // scrolling when QS is expanded. 437 if (mScrollView.isDispatchingTouchEvent()) { 438 return; 439 } 440 super.requestDisallowInterceptTouchEvent(disallowIntercept); 441 } 442 443 private void flingQsWithCurrentVelocity() { 444 float vel = getCurrentVelocity(); 445 flingSettings(vel, flingExpandsQs(vel)); 446 } 447 448 private boolean flingExpandsQs(float vel) { 449 if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 450 return getQsExpansionFraction() > 0.5f; 451 } else { 452 return vel > 0; 453 } 454 } 455 456 private float getQsExpansionFraction() { 457 return (mQsExpansionHeight - mQsMinExpansionHeight) 458 / (getTempQsMaxExpansion() - mQsMinExpansionHeight); 459 } 460 461 @Override 462 public boolean onTouchEvent(MotionEvent event) { 463 if (mBlockTouches) { 464 return false; 465 } 466 // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference 467 // implementation. 468 if ((!mIsExpanding || mHintAnimationRunning) 469 && !mQsExpanded 470 && mStatusBar.getBarState() != StatusBarState.SHADE) { 471 mPageSwiper.onTouchEvent(event); 472 if (mPageSwiper.isSwipingInProgress()) { 473 return true; 474 } 475 } 476 if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f 477 && mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 478 479 // Down in the empty area while fully expanded - go to QS. 480 mQsTracking = true; 481 mConflictingQsExpansionGesture = true; 482 onQsExpansionStarted(); 483 mInitialHeightOnTouch = mQsExpansionHeight; 484 mInitialTouchY = event.getX(); 485 mInitialTouchX = event.getY(); 486 } 487 if (mQsTracking || mQsExpanded) { 488 onQsTouch(event); 489 if (!mConflictingQsExpansionGesture) { 490 return true; 491 } 492 } 493 if (event.getActionMasked() == MotionEvent.ACTION_CANCEL 494 || event.getActionMasked() == MotionEvent.ACTION_UP) { 495 mConflictingQsExpansionGesture = false; 496 } 497 super.onTouchEvent(event); 498 return true; 499 } 500 501 @Override 502 protected boolean flingExpands(float vel, float vectorVel) { 503 boolean expands = super.flingExpands(vel, vectorVel); 504 505 // If we are already running a QS expansion, make sure that we keep the panel open. 506 if (mQsExpansionAnimator != null) { 507 expands = true; 508 } 509 return expands; 510 } 511 512 @Override 513 protected boolean hasConflictingGestures() { 514 return mStatusBar.getBarState() != StatusBarState.SHADE; 515 } 516 517 private void onQsTouch(MotionEvent event) { 518 int pointerIndex = event.findPointerIndex(mTrackingPointer); 519 if (pointerIndex < 0) { 520 pointerIndex = 0; 521 mTrackingPointer = event.getPointerId(pointerIndex); 522 } 523 final float y = event.getY(pointerIndex); 524 final float x = event.getX(pointerIndex); 525 526 switch (event.getActionMasked()) { 527 case MotionEvent.ACTION_DOWN: 528 mQsTracking = true; 529 mInitialTouchY = y; 530 mInitialTouchX = x; 531 onQsExpansionStarted(); 532 mInitialHeightOnTouch = mQsExpansionHeight; 533 initVelocityTracker(); 534 trackMovement(event); 535 break; 536 537 case MotionEvent.ACTION_POINTER_UP: 538 final int upPointer = event.getPointerId(event.getActionIndex()); 539 if (mTrackingPointer == upPointer) { 540 // gesture is ongoing, find a new pointer to track 541 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 542 final float newY = event.getY(newIndex); 543 final float newX = event.getX(newIndex); 544 mTrackingPointer = event.getPointerId(newIndex); 545 mInitialHeightOnTouch = mQsExpansionHeight; 546 mInitialTouchY = newY; 547 mInitialTouchX = newX; 548 } 549 break; 550 551 case MotionEvent.ACTION_MOVE: 552 final float h = y - mInitialTouchY; 553 setQsExpansion(h + mInitialHeightOnTouch); 554 trackMovement(event); 555 break; 556 557 case MotionEvent.ACTION_UP: 558 case MotionEvent.ACTION_CANCEL: 559 mQsTracking = false; 560 mTrackingPointer = -1; 561 trackMovement(event); 562 float fraction = getQsExpansionFraction(); 563 if ((fraction != 0f || y >= mInitialTouchY) 564 && (fraction != 1f || y <= mInitialTouchY)) { 565 flingQsWithCurrentVelocity(); 566 } else { 567 mScrollYOverride = -1; 568 } 569 if (mVelocityTracker != null) { 570 mVelocityTracker.recycle(); 571 mVelocityTracker = null; 572 } 573 break; 574 } 575 } 576 577 @Override 578 public void onOverscrolled(int amount) { 579 if (mIntercepting) { 580 onQsExpansionStarted(amount); 581 mInitialHeightOnTouch = mQsExpansionHeight; 582 mInitialTouchY = mLastTouchY; 583 mInitialTouchX = mLastTouchX; 584 mQsTracking = true; 585 } 586 } 587 588 @Override 589 public void onOverscrollTopChanged(float amount, boolean isRubberbanded) { 590 cancelAnimation(); 591 float rounded = amount >= 1f ? amount : 0f; 592 mStackScrollerOverscrolling = rounded != 0f && isRubberbanded; 593 mQsExpansionFromOverscroll = rounded != 0f; 594 setQsExpansion(mQsMinExpansionHeight + rounded); 595 updateQsState(); 596 } 597 598 @Override 599 public void flingTopOverscroll(float velocity, boolean open) { 600 setQsExpansion(mQsExpansionHeight); 601 flingSettings(velocity, open, new Runnable() { 602 @Override 603 public void run() { 604 mStackScrollerOverscrolling = false; 605 mQsExpansionFromOverscroll = false; 606 updateQsState(); 607 } 608 }); 609 } 610 611 private void onQsExpansionStarted() { 612 onQsExpansionStarted(0); 613 } 614 615 private void onQsExpansionStarted(int overscrollAmount) { 616 cancelAnimation(); 617 618 // Reset scroll position and apply that position to the expanded height. 619 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 620 if (mScrollView.getScrollY() != 0) { 621 mScrollYOverride = mScrollView.getScrollY(); 622 } 623 mScrollView.scrollTo(0, 0); 624 setQsExpansion(height); 625 } 626 627 private void setQsExpanded(boolean expanded) { 628 boolean changed = mQsExpanded != expanded; 629 if (changed) { 630 mQsExpanded = expanded; 631 updateQsState(); 632 requestPanelHeightUpdate(); 633 mNotificationStackScroller.setInterceptDelegateEnabled(expanded); 634 mStatusBar.setQsExpanded(expanded); 635 } 636 } 637 638 public void setBarState(int statusBarState) { 639 boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD 640 || statusBarState == StatusBarState.SHADE_LOCKED; 641 if (!mKeyguardShowing && keyguardShowing) { 642 setQsTranslation(mQsExpansionHeight); 643 mHeader.setTranslationY(0f); 644 } 645 mStatusBarState = statusBarState; 646 mKeyguardShowing = keyguardShowing; 647 updateQsState(); 648 } 649 650 private void updateQsState() { 651 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; 652 mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling); 653 mNotificationStackScroller.setScrollingEnabled(mStatusBarState != StatusBarState.KEYGUARD 654 && (!mQsExpanded || mQsExpansionFromOverscroll)); 655 mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 656 mQsContainer.setVisibility( 657 mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE); 658 mScrollView.setTouchEnabled(mQsExpanded); 659 } 660 661 private void setQsExpansion(float height) { 662 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); 663 mQsFullyExpanded = height == mQsMaxExpansionHeight; 664 if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) { 665 setQsExpanded(true); 666 } else if (height <= mQsMinExpansionHeight && mQsExpanded) { 667 setQsExpanded(false); 668 } 669 mQsExpansionHeight = height; 670 mHeader.setExpansion(getQsExpansionFraction()); 671 setQsTranslation(height); 672 requestScrollerTopPaddingUpdate(false /* animate */); 673 updateNotificationScrim(height); 674 } 675 676 private void updateNotificationScrim(float height) { 677 int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance; 678 float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance); 679 progress = Math.max(0.0f, Math.min(progress, 1.0f)); 680 mNotificationStackScroller.setScrimAlpha(progress); 681 } 682 683 private void setQsTranslation(float height) { 684 mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation()); 685 } 686 687 private void requestScrollerTopPaddingUpdate(boolean animate) { 688 mNotificationStackScroller.updateTopPadding(mQsExpansionHeight, 689 mScrollView.getScrollY(), 690 mAnimateNextTopPaddingChange || animate); 691 mAnimateNextTopPaddingChange = false; 692 } 693 694 private void trackMovement(MotionEvent event) { 695 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 696 mLastTouchX = event.getX(); 697 mLastTouchY = event.getY(); 698 } 699 700 private void initVelocityTracker() { 701 if (mVelocityTracker != null) { 702 mVelocityTracker.recycle(); 703 } 704 mVelocityTracker = VelocityTracker.obtain(); 705 } 706 707 private float getCurrentVelocity() { 708 if (mVelocityTracker == null) { 709 return 0; 710 } 711 mVelocityTracker.computeCurrentVelocity(1000); 712 return mVelocityTracker.getYVelocity(); 713 } 714 715 private void cancelAnimation() { 716 if (mQsExpansionAnimator != null) { 717 mQsExpansionAnimator.cancel(); 718 } 719 } 720 721 private void flingSettings(float vel, boolean expand) { 722 flingSettings(vel, expand, null); 723 } 724 725 private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) { 726 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 727 if (target == mQsExpansionHeight) { 728 mScrollYOverride = -1; 729 if (onFinishRunnable != null) { 730 onFinishRunnable.run(); 731 } 732 return; 733 } 734 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 735 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 736 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 737 @Override 738 public void onAnimationUpdate(ValueAnimator animation) { 739 setQsExpansion((Float) animation.getAnimatedValue()); 740 } 741 }); 742 animator.addListener(new AnimatorListenerAdapter() { 743 @Override 744 public void onAnimationEnd(Animator animation) { 745 mScrollYOverride = -1; 746 mQsExpansionAnimator = null; 747 if (onFinishRunnable != null) { 748 onFinishRunnable.run(); 749 } 750 } 751 }); 752 animator.start(); 753 mQsExpansionAnimator = animator; 754 mQsAnimatorExpand = expand; 755 } 756 757 /** 758 * @return Whether we should intercept a gesture to open Quick Settings. 759 */ 760 private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { 761 if (!mQsExpansionEnabled) { 762 return false; 763 } 764 boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight() 765 && y >= mHeader.getTop() && y <= mHeader.getBottom(); 766 if (mQsExpanded) { 767 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0); 768 } else { 769 return onHeader; 770 } 771 } 772 773 @Override 774 public void setVisibility(int visibility) { 775 int oldVisibility = getVisibility(); 776 super.setVisibility(visibility); 777 if (visibility != oldVisibility) { 778 reparentStatusIcons(visibility == VISIBLE); 779 } 780 } 781 782 /** 783 * When the notification panel gets expanded, we need to move the status icons in the header 784 * card. 785 */ 786 private void reparentStatusIcons(boolean toHeader) { 787 if (mStatusBar == null) { 788 return; 789 } 790 LinearLayout systemIcons = mStatusBar.getSystemIcons(); 791 ViewGroup parent = ((ViewGroup) systemIcons.getParent()); 792 if (toHeader) { 793 int index = parent.indexOfChild(systemIcons); 794 parent.removeView(systemIcons); 795 mSystemIconsCopy.setMirroredView( 796 systemIcons, systemIcons.getWidth(), systemIcons.getHeight()); 797 parent.addView(mSystemIconsCopy, index); 798 mHeader.attachSystemIcons(systemIcons); 799 } else { 800 ViewGroup newParent = mStatusBar.getSystemIconArea(); 801 int index = newParent.indexOfChild(mSystemIconsCopy); 802 parent.removeView(systemIcons); 803 mHeader.onSystemIconsDetached(); 804 mSystemIconsCopy.setMirroredView(null, 0, 0); 805 newParent.removeView(mSystemIconsCopy); 806 newParent.addView(systemIcons, index); 807 } 808 } 809 810 @Override 811 protected boolean isScrolledToBottom() { 812 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 813 return true; 814 } 815 if (!isInSettings()) { 816 return mNotificationStackScroller.isScrolledToBottom(); 817 } 818 return super.isScrolledToBottom(); 819 } 820 821 @Override 822 protected int getMaxPanelHeight() { 823 int min = mStatusBarMinHeight; 824 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD 825 && mNotificationStackScroller.getNotGoneChildCount() == 0) { 826 int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount()) 827 * HEADER_RUBBERBAND_FACTOR); 828 min = Math.max(min, minHeight); 829 } 830 int maxHeight; 831 if (mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) { 832 maxHeight = (int) calculatePanelHeightQsExpanded(); 833 } else { 834 int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin(); 835 maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin 836 - mTopPaddingAdjustment; 837 maxHeight += mNotificationStackScroller.getTopPaddingOverflow(); 838 } 839 maxHeight = Math.max(maxHeight, min); 840 return maxHeight; 841 } 842 843 private boolean isInSettings() { 844 return mQsExpanded; 845 } 846 847 @Override 848 protected void onHeightUpdated(float expandedHeight) { 849 if (!mQsExpanded) { 850 positionClockAndNotifications(); 851 } 852 if (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null 853 && !mQsExpansionFromOverscroll) { 854 float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding() 855 + mNotificationStackScroller.getMinStackHeight() 856 + mNotificationStackScroller.getNotificationTopPadding(); 857 float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); 858 float t = (expandedHeight - panelHeightQsCollapsed) 859 / (panelHeightQsExpanded - panelHeightQsCollapsed); 860 861 setQsExpansion(mQsMinExpansionHeight 862 + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight)); 863 } 864 mNotificationStackScroller.setStackHeight(expandedHeight); 865 updateHeader(); 866 updateUnlockIcon(); 867 updateNotificationTranslucency(); 868 } 869 870 /** 871 * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when 872 * collapsing QS / the panel when QS was scrolled 873 */ 874 private int getTempQsMaxExpansion() { 875 int qsTempMaxExpansion = mQsMaxExpansionHeight; 876 if (mScrollYOverride != -1) { 877 qsTempMaxExpansion -= mScrollYOverride; 878 } 879 return qsTempMaxExpansion; 880 } 881 882 private float calculatePanelHeightQsExpanded() { 883 float notificationHeight = mNotificationStackScroller.getHeight() 884 - mNotificationStackScroller.getEmptyBottomMargin() 885 - mNotificationStackScroller.getTopPadding(); 886 float totalHeight = mQsMaxExpansionHeight + notificationHeight 887 + mNotificationStackScroller.getNotificationTopPadding(); 888 if (totalHeight > mNotificationStackScroller.getHeight()) { 889 float fullyCollapsedHeight = mQsMaxExpansionHeight 890 + mNotificationStackScroller.getMinStackHeight() 891 + mNotificationStackScroller.getNotificationTopPadding() 892 - getScrollViewScrollY(); 893 totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight()); 894 } 895 return totalHeight; 896 } 897 898 private int getScrollViewScrollY() { 899 if (mScrollYOverride != -1) { 900 return mScrollYOverride; 901 } else { 902 return mScrollView.getScrollY(); 903 } 904 } 905 private void updateNotificationTranslucency() { 906 float alpha = (mNotificationStackScroller.getNotificationsTopY() 907 + mNotificationStackScroller.getItemHeight()) 908 / (mQsMinExpansionHeight 909 + mNotificationStackScroller.getItemHeight() / 2); 910 alpha = Math.max(0, Math.min(alpha, 1)); 911 alpha = (float) Math.pow(alpha, 0.75); 912 913 // TODO: Draw a rect with DST_OUT over the notifications to achieve the same effect - 914 // this would be much more efficient. 915 mNotificationStackScroller.setAlpha(alpha); 916 } 917 918 @Override 919 protected float getOverExpansionAmount() { 920 return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */); 921 } 922 923 @Override 924 protected float getOverExpansionPixels() { 925 return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */); 926 } 927 928 private void updateUnlockIcon() { 929 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 930 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 931 boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance; 932 if (active && !mUnlockIconActive && mTracking) { 933 mKeyguardBottomArea.getLockIcon().animate() 934 .alpha(1f) 935 .scaleY(LOCK_ICON_ACTIVE_SCALE) 936 .scaleX(LOCK_ICON_ACTIVE_SCALE) 937 .setInterpolator(mFastOutLinearInterpolator) 938 .setDuration(150); 939 } else if (!active && mUnlockIconActive && mTracking) { 940 mKeyguardBottomArea.getLockIcon().animate() 941 .alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT) 942 .scaleY(1f) 943 .scaleX(1f) 944 .setInterpolator(mFastOutLinearInterpolator) 945 .setDuration(150); 946 } 947 mUnlockIconActive = active; 948 } 949 } 950 951 /** 952 * Hides the header when notifications are colliding with it. 953 */ 954 private void updateHeader() { 955 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 956 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 957 updateHeaderKeyguard(); 958 } else { 959 updateHeaderShade(); 960 } 961 962 } 963 964 private void updateHeaderShade() { 965 mHeader.setAlpha(1f); 966 mHeader.setTranslationY(getHeaderTranslation()); 967 setQsTranslation(mQsExpansionHeight); 968 } 969 970 private float getHeaderTranslation() { 971 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 972 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 973 return 0; 974 } 975 if (mNotificationStackScroller.getNotGoneChildCount() == 0) { 976 if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) { 977 return 0; 978 } else { 979 return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight; 980 } 981 } 982 return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR; 983 } 984 985 private void updateHeaderKeyguard() { 986 mHeader.setTranslationY(0f); 987 float alpha; 988 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 989 990 // When on Keyguard, we hide the header as soon as the top card of the notification 991 // stack scroller is close enough (collision distance) to the bottom of the header. 992 alpha = mNotificationStackScroller.getNotificationsTopY() 993 / 994 (mQsMinExpansionHeight + mNotificationsHeaderCollideDistance); 995 996 } else { 997 998 // In SHADE_LOCKED, the top card is already really close to the header. Hide it as 999 // soon as we start translating the stack. 1000 alpha = mNotificationStackScroller.getNotificationsTopY() / mQsMinExpansionHeight; 1001 } 1002 alpha = Math.max(0, Math.min(alpha, 1)); 1003 alpha = (float) Math.pow(alpha, 0.75); 1004 mHeader.setAlpha(alpha); 1005 mKeyguardBottomArea.setAlpha(alpha); 1006 setQsTranslation(mQsExpansionHeight); 1007 } 1008 1009 @Override 1010 protected void onExpandingStarted() { 1011 super.onExpandingStarted(); 1012 mNotificationStackScroller.onExpansionStarted(); 1013 mIsExpanding = true; 1014 mQsExpandedWhenExpandingStarted = mQsExpanded; 1015 if (mQsExpanded) { 1016 onQsExpansionStarted(); 1017 } 1018 } 1019 1020 @Override 1021 protected void onExpandingFinished() { 1022 super.onExpandingFinished(); 1023 mNotificationStackScroller.onExpansionStopped(); 1024 mIsExpanding = false; 1025 mScrollYOverride = -1; 1026 if (mExpandedHeight == 0f) { 1027 mHeader.setListening(false); 1028 mQsPanel.setListening(false); 1029 } else { 1030 mHeader.setListening(true); 1031 mQsPanel.setListening(true); 1032 } 1033 } 1034 1035 @Override 1036 public void instantExpand() { 1037 super.instantExpand(); 1038 mHeader.setListening(true); 1039 mQsPanel.setListening(true); 1040 } 1041 1042 @Override 1043 protected void setOverExpansion(float overExpansion, boolean isPixels) { 1044 if (mConflictingQsExpansionGesture) { 1045 return; 1046 } 1047 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 1048 mNotificationStackScroller.setOnHeightChangedListener(null); 1049 if (isPixels) { 1050 mNotificationStackScroller.setOverScrolledPixels( 1051 overExpansion, true /* onTop */, false /* animate */); 1052 } else { 1053 mNotificationStackScroller.setOverScrollAmount( 1054 overExpansion, true /* onTop */, false /* animate */); 1055 } 1056 mNotificationStackScroller.setOnHeightChangedListener(this); 1057 } 1058 } 1059 1060 @Override 1061 protected void onTrackingStarted() { 1062 super.onTrackingStarted(); 1063 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1064 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 1065 mPageSwiper.animateHideLeftRightIcon(); 1066 } 1067 } 1068 1069 @Override 1070 protected void onTrackingStopped(boolean expand) { 1071 super.onTrackingStopped(expand); 1072 if (expand) { 1073 mNotificationStackScroller.setOverScrolledPixels( 1074 0.0f, true /* onTop */, true /* animate */); 1075 } 1076 if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1077 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1078 mPageSwiper.showAllIcons(true); 1079 } 1080 if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 1081 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 1082 mKeyguardBottomArea.getLockIcon().animate() 1083 .alpha(0f) 1084 .scaleX(2f) 1085 .scaleY(2f) 1086 .setInterpolator(mFastOutLinearInterpolator) 1087 .setDuration(100); 1088 } 1089 } 1090 1091 @Override 1092 public void onHeightChanged(ExpandableView view) { 1093 1094 // Block update if we are in quick settings and just the top padding changed 1095 // (i.e. view == null). 1096 if (view == null && mQsExpanded) { 1097 return; 1098 } 1099 requestPanelHeightUpdate(); 1100 } 1101 1102 @Override 1103 public void onScrollChanged() { 1104 if (mQsExpanded) { 1105 requestScrollerTopPaddingUpdate(false /* animate */); 1106 requestPanelHeightUpdate(); 1107 } 1108 } 1109 1110 @Override 1111 protected void onConfigurationChanged(Configuration newConfig) { 1112 super.onConfigurationChanged(newConfig); 1113 mPageSwiper.onConfigurationChanged(); 1114 } 1115 1116 @Override 1117 public void onClick(View v) { 1118 if (v == mHeader.getBackgroundView()) { 1119 onQsExpansionStarted(); 1120 if (mQsExpanded) { 1121 flingSettings(0 /* vel */, false /* expand */); 1122 } else if (mQsExpansionEnabled) { 1123 flingSettings(0 /* vel */, true /* expand */); 1124 } 1125 } 1126 } 1127 1128 @Override 1129 public void onAnimationToSideStarted(boolean rightPage) { 1130 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; 1131 if (start) { 1132 mKeyguardBottomArea.launchPhone(); 1133 } else { 1134 mKeyguardBottomArea.launchCamera(); 1135 } 1136 mBlockTouches = true; 1137 } 1138 1139 @Override 1140 protected void onEdgeClicked(boolean right) { 1141 if ((right && getRightIcon().getVisibility() != View.VISIBLE) 1142 || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) { 1143 return; 1144 } 1145 mHintAnimationRunning = true; 1146 mPageSwiper.startHintAnimation(right, new Runnable() { 1147 @Override 1148 public void run() { 1149 mHintAnimationRunning = false; 1150 mStatusBar.onHintFinished(); 1151 } 1152 }); 1153 startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon()); 1154 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; 1155 if (start) { 1156 mStatusBar.onPhoneHintStarted(); 1157 } else { 1158 mStatusBar.onCameraHintStarted(); 1159 } 1160 } 1161 1162 @Override 1163 protected void startUnlockHintAnimation() { 1164 super.startUnlockHintAnimation(); 1165 startHighlightIconAnimation(getCenterIcon()); 1166 } 1167 1168 /** 1169 * Starts the highlight (making it fully opaque) animation on an icon. 1170 */ 1171 private void startHighlightIconAnimation(final View icon) { 1172 icon.animate() 1173 .alpha(1.0f) 1174 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 1175 .setInterpolator(mFastOutSlowInInterpolator) 1176 .withEndAction(new Runnable() { 1177 @Override 1178 public void run() { 1179 icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT) 1180 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 1181 .setInterpolator(mFastOutSlowInInterpolator); 1182 } 1183 }); 1184 } 1185 1186 @Override 1187 public float getPageWidth() { 1188 return getWidth(); 1189 } 1190 1191 @Override 1192 public ArrayList<View> getTranslationViews() { 1193 return mSwipeTranslationViews; 1194 } 1195 1196 @Override 1197 public View getLeftIcon() { 1198 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1199 ? mKeyguardBottomArea.getCameraImageView() 1200 : mKeyguardBottomArea.getPhoneImageView(); 1201 } 1202 1203 @Override 1204 public View getCenterIcon() { 1205 return mKeyguardBottomArea.getLockIcon(); 1206 } 1207 1208 @Override 1209 public View getRightIcon() { 1210 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 1211 ? mKeyguardBottomArea.getPhoneImageView() 1212 : mKeyguardBottomArea.getCameraImageView(); 1213 } 1214 1215 @Override 1216 protected float getPeekHeight() { 1217 if (mNotificationStackScroller.getNotGoneChildCount() > 0) { 1218 return mNotificationStackScroller.getPeekHeight(); 1219 } else { 1220 return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR; 1221 } 1222 } 1223 1224 @Override 1225 protected float getCannedFlingDurationFactor() { 1226 if (mQsExpanded) { 1227 return 0.7f; 1228 } else { 1229 return 0.6f; 1230 } 1231 } 1232 1233 @Override 1234 protected boolean isTrackingBlocked() { 1235 return mConflictingQsExpansionGesture && mQsExpanded; 1236 } 1237 1238 public void notifyVisibleChildrenChanged() { 1239 if (mNotificationStackScroller.getNotGoneChildCount() != 0) { 1240 mReserveNotificationSpace.setVisibility(View.VISIBLE); 1241 } else { 1242 mReserveNotificationSpace.setVisibility(View.GONE); 1243 } 1244 } 1245 1246 public boolean isQsExpanded() { 1247 return mQsExpanded; 1248 } 1249} 1250