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