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