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