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