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