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