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