NotificationPanelView.java revision 758afb14976cabf2f6d7be4760cd3d4963070251
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 } 231 232 private void startClockAnimation(int y) { 233 if (mClockAnimationTarget == y) { 234 return; 235 } 236 mClockAnimationTarget = y; 237 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 238 @Override 239 public boolean onPreDraw() { 240 getViewTreeObserver().removeOnPreDrawListener(this); 241 if (mClockAnimator != null) { 242 mClockAnimator.removeAllListeners(); 243 mClockAnimator.cancel(); 244 } 245 mClockAnimator = ObjectAnimator 246 .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget); 247 mClockAnimator.setInterpolator(mFastOutSlowInInterpolator); 248 mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 249 mClockAnimator.addListener(new AnimatorListenerAdapter() { 250 @Override 251 public void onAnimationEnd(Animator animation) { 252 mClockAnimator = null; 253 mClockAnimationTarget = -1; 254 } 255 }); 256 mClockAnimator.start(); 257 return true; 258 } 259 }); 260 } 261 262 private void applyClockAlpha(float alpha) { 263 if (alpha != 1.0f) { 264 mKeyguardStatusView.setLayerType(LAYER_TYPE_HARDWARE, null); 265 } else { 266 mKeyguardStatusView.setLayerType(LAYER_TYPE_NONE, null); 267 } 268 mKeyguardStatusView.setAlpha(alpha); 269 } 270 271 public void animateToFullShade() { 272 mAnimateNextTopPaddingChange = true; 273 mNotificationStackScroller.goToFullShade(); 274 requestLayout(); 275 } 276 277 /** 278 * @return Whether Quick Settings are currently expanded. 279 */ 280 public boolean isQsExpanded() { 281 return mQsExpanded; 282 } 283 284 public void setQsExpansionEnabled(boolean qsExpansionEnabled) { 285 mQsExpansionEnabled = qsExpansionEnabled; 286 } 287 288 @Override 289 public void resetViews() { 290 mBlockTouches = false; 291 mUnlockIconActive = false; 292 mPageSwiper.reset(); 293 closeQs(); 294 } 295 296 public void closeQs() { 297 cancelAnimation(); 298 setQsExpansion(mQsMinExpansionHeight); 299 } 300 301 public void openQs() { 302 cancelAnimation(); 303 if (mQsExpansionEnabled) { 304 setQsExpansion(mQsMaxExpansionHeight); 305 } 306 } 307 308 @Override 309 public void fling(float vel, boolean always) { 310 GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); 311 if (gr != null) { 312 gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); 313 } 314 super.fling(vel, always); 315 } 316 317 @Override 318 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 319 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 320 event.getText() 321 .add(getContext().getString(R.string.accessibility_desc_notification_shade)); 322 return true; 323 } 324 325 return super.dispatchPopulateAccessibilityEvent(event); 326 } 327 328 @Override 329 public boolean onInterceptTouchEvent(MotionEvent event) { 330 if (mBlockTouches) { 331 return false; 332 } 333 int pointerIndex = event.findPointerIndex(mTrackingPointer); 334 if (pointerIndex < 0) { 335 pointerIndex = 0; 336 mTrackingPointer = event.getPointerId(pointerIndex); 337 } 338 final float x = event.getX(pointerIndex); 339 final float y = event.getY(pointerIndex); 340 341 switch (event.getActionMasked()) { 342 case MotionEvent.ACTION_DOWN: 343 mIntercepting = true; 344 mInitialTouchY = y; 345 mInitialTouchX = x; 346 initVelocityTracker(); 347 trackMovement(event); 348 if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { 349 getParent().requestDisallowInterceptTouchEvent(true); 350 } 351 break; 352 case MotionEvent.ACTION_POINTER_UP: 353 final int upPointer = event.getPointerId(event.getActionIndex()); 354 if (mTrackingPointer == upPointer) { 355 // gesture is ongoing, find a new pointer to track 356 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 357 mTrackingPointer = event.getPointerId(newIndex); 358 mInitialTouchX = event.getX(newIndex); 359 mInitialTouchY = event.getY(newIndex); 360 } 361 break; 362 363 case MotionEvent.ACTION_MOVE: 364 final float h = y - mInitialTouchY; 365 trackMovement(event); 366 if (mQsTracking) { 367 368 // Already tracking because onOverscrolled was called. We need to update here 369 // so we don't stop for a frame until the next touch event gets handled in 370 // onTouchEvent. 371 setQsExpansion(h + mInitialHeightOnTouch); 372 trackMovement(event); 373 mIntercepting = false; 374 return true; 375 } 376 if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX) 377 && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { 378 onQsExpansionStarted(); 379 mInitialHeightOnTouch = mQsExpansionHeight; 380 mInitialTouchY = y; 381 mInitialTouchX = x; 382 mQsTracking = true; 383 mIntercepting = false; 384 mNotificationStackScroller.removeLongPressCallback(); 385 return true; 386 } 387 break; 388 389 case MotionEvent.ACTION_CANCEL: 390 case MotionEvent.ACTION_UP: 391 trackMovement(event); 392 if (mQsTracking) { 393 flingQsWithCurrentVelocity(); 394 mQsTracking = false; 395 } 396 mIntercepting = false; 397 break; 398 } 399 return !mQsExpanded && super.onInterceptTouchEvent(event); 400 } 401 402 @Override 403 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 404 405 // Block request when interacting with the scroll view so we can still intercept the 406 // scrolling when QS is expanded. 407 if (mScrollView.isDispatchingTouchEvent()) { 408 return; 409 } 410 super.requestDisallowInterceptTouchEvent(disallowIntercept); 411 } 412 413 private void flingQsWithCurrentVelocity() { 414 float vel = getCurrentVelocity(); 415 416 // TODO: Better logic whether we should expand or not. 417 flingSettings(vel, vel > 0); 418 } 419 420 @Override 421 public boolean onTouchEvent(MotionEvent event) { 422 if (mBlockTouches) { 423 return false; 424 } 425 // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference 426 // implementation. 427 if ((!mIsExpanding || mHintAnimationRunning) 428 && !mQsExpanded 429 && mStatusBar.getBarState() != StatusBarState.SHADE) { 430 mPageSwiper.onTouchEvent(event); 431 if (mPageSwiper.isSwipingInProgress()) { 432 return true; 433 } 434 } 435 if (mQsTracking || mQsExpanded) { 436 return onQsTouch(event); 437 } 438 439 super.onTouchEvent(event); 440 return true; 441 } 442 443 @Override 444 protected boolean hasConflictingGestures() { 445 return mStatusBar.getBarState() != StatusBarState.SHADE; 446 } 447 448 private boolean onQsTouch(MotionEvent event) { 449 int pointerIndex = event.findPointerIndex(mTrackingPointer); 450 if (pointerIndex < 0) { 451 pointerIndex = 0; 452 mTrackingPointer = event.getPointerId(pointerIndex); 453 } 454 final float y = event.getY(pointerIndex); 455 final float x = event.getX(pointerIndex); 456 457 switch (event.getActionMasked()) { 458 case MotionEvent.ACTION_DOWN: 459 mQsTracking = true; 460 mInitialTouchY = y; 461 mInitialTouchX = x; 462 onQsExpansionStarted(); 463 mInitialHeightOnTouch = mQsExpansionHeight; 464 initVelocityTracker(); 465 trackMovement(event); 466 break; 467 468 case MotionEvent.ACTION_POINTER_UP: 469 final int upPointer = event.getPointerId(event.getActionIndex()); 470 if (mTrackingPointer == upPointer) { 471 // gesture is ongoing, find a new pointer to track 472 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 473 final float newY = event.getY(newIndex); 474 final float newX = event.getX(newIndex); 475 mTrackingPointer = event.getPointerId(newIndex); 476 mInitialHeightOnTouch = mQsExpansionHeight; 477 mInitialTouchY = newY; 478 mInitialTouchX = newX; 479 } 480 break; 481 482 case MotionEvent.ACTION_MOVE: 483 final float h = y - mInitialTouchY; 484 setQsExpansion(h + mInitialHeightOnTouch); 485 trackMovement(event); 486 break; 487 488 case MotionEvent.ACTION_UP: 489 case MotionEvent.ACTION_CANCEL: 490 mQsTracking = false; 491 mTrackingPointer = -1; 492 trackMovement(event); 493 flingQsWithCurrentVelocity(); 494 if (mVelocityTracker != null) { 495 mVelocityTracker.recycle(); 496 mVelocityTracker = null; 497 } 498 break; 499 } 500 return true; 501 } 502 503 @Override 504 public void onOverscrolled(int amount) { 505 if (mIntercepting) { 506 onQsExpansionStarted(amount); 507 mInitialHeightOnTouch = mQsExpansionHeight; 508 mInitialTouchY = mLastTouchY; 509 mInitialTouchX = mLastTouchX; 510 mQsTracking = true; 511 } 512 } 513 514 515 @Override 516 public void onOverscrollTopChanged(float amount) { 517 cancelAnimation(); 518 float rounded = amount >= 1f ? amount : 0f; 519 mStackScrollerOverscrolling = rounded != 0f; 520 setQsExpansion(mQsMinExpansionHeight + rounded); 521 updateQsState(); 522 } 523 524 @Override 525 public void flingTopOverscroll(float velocity, boolean open) { 526 mStackScrollerOverscrolling = false; 527 setQsExpansion(mQsExpansionHeight); 528 flingSettings(velocity, open); 529 } 530 531 private void onQsExpansionStarted() { 532 onQsExpansionStarted(0); 533 } 534 535 private void onQsExpansionStarted(int overscrollAmount) { 536 cancelAnimation(); 537 538 // Reset scroll position and apply that position to the expanded height. 539 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 540 mScrollView.scrollTo(0, 0); 541 setQsExpansion(height); 542 } 543 544 private void setQsExpanded(boolean expanded) { 545 boolean changed = mQsExpanded != expanded; 546 if (changed) { 547 mQsExpanded = expanded; 548 updateQsState(); 549 } 550 } 551 552 public void setKeyguardShowing(boolean keyguardShowing) { 553 mKeyguardShowing = keyguardShowing; 554 updateQsState(); 555 } 556 557 private void updateQsState() { 558 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; 559 mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling); 560 mNotificationStackScroller.setEnabled(!mQsExpanded); 561 mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 562 mQsContainer.setVisibility(mKeyguardShowing && !expandVisually 563 ? View.INVISIBLE 564 : View.VISIBLE); 565 mScrollView.setTouchEnabled(mQsExpanded); 566 mNotificationStackScroller.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 mAnimateNextTopPaddingChange = false; 594 } 595 596 private void trackMovement(MotionEvent event) { 597 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 598 mLastTouchX = event.getX(); 599 mLastTouchY = event.getY(); 600 } 601 602 private void initVelocityTracker() { 603 if (mVelocityTracker != null) { 604 mVelocityTracker.recycle(); 605 } 606 mVelocityTracker = VelocityTracker.obtain(); 607 } 608 609 private float getCurrentVelocity() { 610 if (mVelocityTracker == null) { 611 return 0; 612 } 613 mVelocityTracker.computeCurrentVelocity(1000); 614 return mVelocityTracker.getYVelocity(); 615 } 616 617 private void cancelAnimation() { 618 if (mQsExpansionAnimator != null) { 619 mQsExpansionAnimator.cancel(); 620 } 621 } 622 private void flingSettings(float vel, boolean expand) { 623 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 624 if (target == mQsExpansionHeight) { 625 return; 626 } 627 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 628 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 629 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 630 @Override 631 public void onAnimationUpdate(ValueAnimator animation) { 632 setQsExpansion((Float) animation.getAnimatedValue()); 633 } 634 }); 635 animator.addListener(new AnimatorListenerAdapter() { 636 @Override 637 public void onAnimationEnd(Animator animation) { 638 mQsExpansionAnimator = null; 639 } 640 }); 641 animator.start(); 642 mQsExpansionAnimator = animator; 643 } 644 645 /** 646 * @return Whether we should intercept a gesture to open Quick Settings. 647 */ 648 private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { 649 if (!mQsExpansionEnabled) { 650 return false; 651 } 652 boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight() 653 && y >= mHeader.getTop() && y <= mHeader.getBottom(); 654 if (mQsExpanded) { 655 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0); 656 } else { 657 return onHeader; 658 } 659 } 660 661 @Override 662 public void setVisibility(int visibility) { 663 int oldVisibility = getVisibility(); 664 super.setVisibility(visibility); 665 if (visibility != oldVisibility) { 666 reparentStatusIcons(visibility == VISIBLE); 667 } 668 } 669 670 /** 671 * When the notification panel gets expanded, we need to move the status icons in the header 672 * card. 673 */ 674 private void reparentStatusIcons(boolean toHeader) { 675 if (mStatusBar == null) { 676 return; 677 } 678 LinearLayout systemIcons = mStatusBar.getSystemIcons(); 679 if (systemIcons.getParent() != null) { 680 ((ViewGroup) systemIcons.getParent()).removeView(systemIcons); 681 } 682 if (toHeader) { 683 mHeader.attachSystemIcons(systemIcons); 684 } else { 685 mHeader.onSystemIconsDetached(); 686 mStatusBar.reattachSystemIcons(); 687 } 688 } 689 690 @Override 691 protected boolean isScrolledToBottom() { 692 if (!isInSettings()) { 693 return mNotificationStackScroller.isScrolledToBottom(); 694 } 695 return super.isScrolledToBottom(); 696 } 697 698 @Override 699 protected int getMaxPanelHeight() { 700 // TODO: Figure out transition for collapsing when QS is open, adjust height here. 701 int maxPanelHeight = super.getMaxPanelHeight(); 702 int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin(); 703 emptyBottomMargin += mStackScrollerContainer.getHeight() 704 - mNotificationStackScroller.getHeight(); 705 int maxHeight = maxPanelHeight - emptyBottomMargin - mTopPaddingAdjustment; 706 maxHeight = Math.max(maxHeight, mStatusBarMinHeight); 707 return maxHeight; 708 } 709 710 private boolean isInSettings() { 711 return mQsExpanded; 712 } 713 714 @Override 715 protected void onHeightUpdated(float expandedHeight) { 716 if (!mQsExpanded) { 717 positionClockAndNotifications(); 718 } 719 mNotificationStackScroller.setStackHeight(expandedHeight); 720 updateKeyguardHeaderVisibility(); 721 updateUnlockIcon(); 722 } 723 724 private void updateUnlockIcon() { 725 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 726 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 727 boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance; 728 if (active && !mUnlockIconActive && mTracking) { 729 mKeyguardBottomArea.getLockIcon().animate() 730 .alpha(1f) 731 .scaleY(LOCK_ICON_ACTIVE_SCALE) 732 .scaleX(LOCK_ICON_ACTIVE_SCALE) 733 .setInterpolator(mFastOutLinearInterpolator) 734 .setDuration(150); 735 } else if (!active && mUnlockIconActive && mTracking) { 736 mKeyguardBottomArea.getLockIcon().animate() 737 .alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT) 738 .scaleY(1f) 739 .scaleX(1f) 740 .setInterpolator(mFastOutLinearInterpolator) 741 .setDuration(150); 742 } 743 mUnlockIconActive = active; 744 } 745 } 746 747 /** 748 * Hides the header when notifications are colliding with it. 749 */ 750 private void updateKeyguardHeaderVisibility() { 751 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 752 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 753 boolean hidden; 754 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 755 756 // When on Keyguard, we hide the header as soon as the top card of the notification 757 // stack scroller is close enough (collision distance) to the bottom of the header. 758 hidden = mNotificationStackScroller.getNotificationsTopY() 759 <= mHeader.getBottom() + mNotificationsHeaderCollideDistance; 760 } else { 761 762 // In SHADE_LOCKED, the top card is already really close to the header. Hide it as 763 // soon as we start translating the stack. 764 hidden = mNotificationStackScroller.getTranslationY() < 0; 765 } 766 767 if (hidden && !mHeaderHidden) { 768 mHeader.animate() 769 .alpha(0f) 770 .withLayer() 771 .translationY(-mHeader.getHeight()/2) 772 .setInterpolator(mFastOutLinearInterpolator) 773 .setDuration(200); 774 } else if (!hidden && mHeaderHidden) { 775 mHeader.animate() 776 .alpha(1f) 777 .withLayer() 778 .translationY(0) 779 .setInterpolator(mLinearOutSlowInInterpolator) 780 .setDuration(200); 781 } 782 mHeaderHidden = hidden; 783 } else { 784 mHeader.animate().cancel(); 785 mHeader.setAlpha(1f); 786 mHeader.setTranslationY(0f); 787 if (mHeader.getLayerType() != LAYER_TYPE_NONE) { 788 mHeader.setLayerType(LAYER_TYPE_NONE, null); 789 } 790 mHeaderHidden = false; 791 } 792 793 } 794 795 @Override 796 protected void onExpandingStarted() { 797 super.onExpandingStarted(); 798 mNotificationStackScroller.onExpansionStarted(); 799 mIsExpanding = true; 800 } 801 802 @Override 803 protected void onExpandingFinished() { 804 super.onExpandingFinished(); 805 mNotificationStackScroller.onExpansionStopped(); 806 mIsExpanding = false; 807 } 808 809 @Override 810 protected void onOverExpansionChanged(float overExpansion) { 811 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 812 float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true); 813 float expansionChange = overExpansion - mOverExpansion; 814 expansionChange *= EXPANSION_RUBBER_BAND_EXTRA_FACTOR; 815 mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + expansionChange, 816 true /* onTop */, 817 false /* animate */); 818 } 819 } 820 821 @Override 822 protected void onTrackingStarted() { 823 super.onTrackingStarted(); 824 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 825 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 826 mPageSwiper.animateHideLeftRightIcon(); 827 } 828 } 829 830 @Override 831 protected void onTrackingStopped(boolean expand) { 832 super.onTrackingStopped(expand); 833 mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */, true /* animate */); 834 if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 835 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 836 mPageSwiper.showAllIcons(true); 837 } 838 if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 839 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 840 mKeyguardBottomArea.getLockIcon().animate() 841 .alpha(0f) 842 .scaleX(2f) 843 .scaleY(2f) 844 .setInterpolator(mFastOutLinearInterpolator) 845 .setDuration(100); 846 } 847 } 848 849 @Override 850 public void onHeightChanged(ExpandableView view) { 851 requestPanelHeightUpdate(); 852 } 853 854 @Override 855 public void onScrollChanged() { 856 if (mQsExpanded) { 857 requestScrollerTopPaddingUpdate(false /* animate */); 858 } 859 } 860 861 @Override 862 protected void onConfigurationChanged(Configuration newConfig) { 863 super.onConfigurationChanged(newConfig); 864 mPageSwiper.onConfigurationChanged(); 865 } 866 867 @Override 868 public void onClick(View v) { 869 if (v == mHeader.getBackgroundView()) { 870 onQsExpansionStarted(); 871 if (mQsExpanded) { 872 flingSettings(0 /* vel */, false /* expand */); 873 } else if (mQsExpansionEnabled) { 874 flingSettings(0 /* vel */, true /* expand */); 875 } 876 } 877 } 878 879 @Override 880 public void onAnimationToSideStarted(boolean rightPage) { 881 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; 882 if (start) { 883 mKeyguardBottomArea.launchPhone(); 884 } else { 885 mKeyguardBottomArea.launchCamera(); 886 } 887 mBlockTouches = true; 888 } 889 890 @Override 891 protected void onEdgeClicked(boolean right) { 892 if ((right && getRightIcon().getVisibility() != View.VISIBLE) 893 || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) { 894 return; 895 } 896 mHintAnimationRunning = true; 897 mPageSwiper.startHintAnimation(right, new Runnable() { 898 @Override 899 public void run() { 900 mHintAnimationRunning = false; 901 mStatusBar.onHintFinished(); 902 } 903 }); 904 startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon()); 905 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; 906 if (start) { 907 mStatusBar.onPhoneHintStarted(); 908 } else { 909 mStatusBar.onCameraHintStarted(); 910 } 911 } 912 913 @Override 914 protected void startUnlockHintAnimation() { 915 super.startUnlockHintAnimation(); 916 startHighlightIconAnimation(getCenterIcon()); 917 } 918 919 /** 920 * Starts the highlight (making it fully opaque) animation on an icon. 921 */ 922 private void startHighlightIconAnimation(final View icon) { 923 icon.animate() 924 .alpha(1.0f) 925 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 926 .setInterpolator(mFastOutSlowInInterpolator) 927 .withEndAction(new Runnable() { 928 @Override 929 public void run() { 930 icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT) 931 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 932 .setInterpolator(mFastOutSlowInInterpolator); 933 } 934 }); 935 } 936 937 @Override 938 public float getPageWidth() { 939 return getWidth(); 940 } 941 942 @Override 943 public ArrayList<View> getTranslationViews() { 944 return mSwipeTranslationViews; 945 } 946 947 @Override 948 public View getLeftIcon() { 949 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 950 ? mKeyguardBottomArea.getCameraImageView() 951 : mKeyguardBottomArea.getPhoneImageView(); 952 } 953 954 @Override 955 public View getCenterIcon() { 956 return mKeyguardBottomArea.getLockIcon(); 957 } 958 959 @Override 960 public View getRightIcon() { 961 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 962 ? mKeyguardBottomArea.getPhoneImageView() 963 : mKeyguardBottomArea.getCameraImageView(); 964 } 965} 966