NotificationPanelView.java revision 8dd95e03f8555c278217bac7e4faef865b1850a7
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 mMinStackHeight; 87 private int mQsPeekHeight; 88 private float mNotificationTranslation; 89 private int mStackScrollerIntrinsicPadding; 90 private boolean mStackScrollerOverscrolling; 91 private boolean mQsExpansionEnabled = true; 92 private ValueAnimator mQsExpansionAnimator; 93 private FlingAnimationUtils mFlingAnimationUtils; 94 private int mStatusBarMinHeight; 95 private boolean mHeaderHidden; 96 private boolean mUnlockIconActive; 97 private int mNotificationsHeaderCollideDistance; 98 private int mUnlockMoveDistance; 99 100 private Interpolator mFastOutSlowInInterpolator; 101 private Interpolator mFastOutLinearInterpolator; 102 private Interpolator mLinearOutSlowInInterpolator; 103 private ObjectAnimator mClockAnimator; 104 private int mClockAnimationTarget = -1; 105 private int mTopPaddingAdjustment; 106 private KeyguardClockPositionAlgorithm mClockPositionAlgorithm = 107 new KeyguardClockPositionAlgorithm(); 108 private KeyguardClockPositionAlgorithm.Result mClockPositionResult = 109 new KeyguardClockPositionAlgorithm.Result(); 110 private boolean mIsSwipedHorizontally; 111 private boolean mIsExpanding; 112 private KeyguardBottomAreaView mKeyguardBottomArea; 113 private boolean mBlockTouches; 114 private ArrayList<View> mSwipeTranslationViews = new ArrayList<>(); 115 116 public NotificationPanelView(Context context, AttributeSet attrs) { 117 super(context, attrs); 118 } 119 120 public void setStatusBar(PhoneStatusBar bar) { 121 if (mStatusBar != null) { 122 mStatusBar.setOnFlipRunnable(null); 123 } 124 mStatusBar = bar; 125 if (bar != null) { 126 mStatusBar.setOnFlipRunnable(new Runnable() { 127 @Override 128 public void run() { 129 requestPanelHeightUpdate(); 130 } 131 }); 132 } 133 } 134 135 @Override 136 protected void onFinishInflate() { 137 super.onFinishInflate(); 138 mHeader = (StatusBarHeaderView) findViewById(R.id.header); 139 mHeader.getBackgroundView().setOnClickListener(this); 140 mHeader.setOverlayParent(this); 141 mKeyguardStatusView = findViewById(R.id.keyguard_status_view); 142 mStackScrollerContainer = findViewById(R.id.notification_container_parent); 143 mQsContainer = findViewById(R.id.quick_settings_container); 144 mQsPanel = findViewById(R.id.quick_settings_panel); 145 mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view); 146 mScrollView.setListener(this); 147 mNotificationStackScroller = (NotificationStackScrollLayout) 148 findViewById(R.id.notification_stack_scroller); 149 mNotificationStackScroller.setOnHeightChangedListener(this); 150 mNotificationStackScroller.setOverscrollTopChangedListener(this); 151 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), 152 android.R.interpolator.fast_out_slow_in); 153 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), 154 android.R.interpolator.linear_out_slow_in); 155 mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(), 156 android.R.interpolator.fast_out_linear_in); 157 mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area); 158 mSwipeTranslationViews.add(mNotificationStackScroller); 159 mSwipeTranslationViews.add(mKeyguardStatusView); 160 mPageSwiper = new KeyguardPageSwipeHelper(this, getContext()); 161 } 162 163 @Override 164 protected void loadDimens() { 165 super.loadDimens(); 166 mNotificationTopPadding = getResources().getDimensionPixelSize( 167 R.dimen.notifications_top_padding); 168 mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height); 169 mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f); 170 mStatusBarMinHeight = getResources().getDimensionPixelSize( 171 com.android.internal.R.dimen.status_bar_height); 172 mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height); 173 mNotificationsHeaderCollideDistance = 174 getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance); 175 mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance); 176 mClockPositionAlgorithm.loadDimens(getResources()); 177 } 178 179 @Override 180 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 181 super.onLayout(changed, left, top, right, bottom); 182 183 // Calculate quick setting heights. 184 mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight; 185 mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight(); 186 if (mQsExpanded) { 187 if (mQsFullyExpanded) { 188 setQsStackScrollerPadding(mQsMaxExpansionHeight); 189 } 190 } else { 191 if (!mStackScrollerOverscrolling) { 192 setQsExpansion(mQsMinExpansionHeight); 193 } 194 positionClockAndNotifications(); 195 mNotificationStackScroller.setStackHeight(getExpandedHeight()); 196 } 197 } 198 199 /** 200 * Positions the clock and notifications dynamically depending on how many notifications are 201 * showing. 202 */ 203 private void positionClockAndNotifications() { 204 boolean animateClock = mNotificationStackScroller.isAddOrRemoveAnimationPending(); 205 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 206 int bottom = mStackScrollerOverscrolling 207 ? mHeader.getCollapsedHeight() 208 : mHeader.getBottom(); 209 mStackScrollerIntrinsicPadding = 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 mStackScrollerIntrinsicPadding = mClockPositionResult.stackScrollerPadding; 228 mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment; 229 } 230 mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding, 231 mAnimateNextTopPaddingChange || animateClock); 232 mAnimateNextTopPaddingChange = false; 233 } 234 235 private void startClockAnimation(int y) { 236 if (mClockAnimationTarget == y) { 237 return; 238 } 239 mClockAnimationTarget = y; 240 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 241 @Override 242 public boolean onPreDraw() { 243 getViewTreeObserver().removeOnPreDrawListener(this); 244 if (mClockAnimator != null) { 245 mClockAnimator.removeAllListeners(); 246 mClockAnimator.cancel(); 247 } 248 mClockAnimator = 249 ObjectAnimator.ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget); 250 mClockAnimator.setInterpolator(mFastOutSlowInInterpolator); 251 mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 252 mClockAnimator.addListener(new AnimatorListenerAdapter() { 253 @Override 254 public void onAnimationEnd(Animator animation) { 255 mClockAnimator = null; 256 mClockAnimationTarget = -1; 257 } 258 }); 259 mClockAnimator.start(); 260 return true; 261 } 262 }); 263 } 264 265 private void applyClockAlpha(float alpha) { 266 if (alpha != 1.0f) { 267 mKeyguardStatusView.setLayerType(LAYER_TYPE_HARDWARE, null); 268 } else { 269 mKeyguardStatusView.setLayerType(LAYER_TYPE_NONE, null); 270 } 271 mKeyguardStatusView.setAlpha(alpha); 272 } 273 274 public void animateToFullShade() { 275 mAnimateNextTopPaddingChange = true; 276 mNotificationStackScroller.goToFullShade(); 277 requestLayout(); 278 } 279 280 /** 281 * @return Whether Quick Settings are currently expanded. 282 */ 283 public boolean isQsExpanded() { 284 return mQsExpanded; 285 } 286 287 public void setQsExpansionEnabled(boolean qsExpansionEnabled) { 288 mQsExpansionEnabled = qsExpansionEnabled; 289 } 290 291 @Override 292 public void resetViews() { 293 mBlockTouches = false; 294 mUnlockIconActive = false; 295 mPageSwiper.reset(); 296 closeQs(); 297 } 298 299 public void closeQs() { 300 cancelAnimation(); 301 setQsExpansion(mQsMinExpansionHeight); 302 } 303 304 public void openQs() { 305 cancelAnimation(); 306 if (mQsExpansionEnabled) { 307 setQsExpansion(mQsMaxExpansionHeight); 308 } 309 } 310 311 @Override 312 public void fling(float vel, boolean always) { 313 GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); 314 if (gr != null) { 315 gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); 316 } 317 super.fling(vel, always); 318 } 319 320 @Override 321 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 322 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 323 event.getText() 324 .add(getContext().getString(R.string.accessibility_desc_notification_shade)); 325 return true; 326 } 327 328 return super.dispatchPopulateAccessibilityEvent(event); 329 } 330 331 @Override 332 public boolean onInterceptTouchEvent(MotionEvent event) { 333 if (mBlockTouches) { 334 return false; 335 } 336 int pointerIndex = event.findPointerIndex(mTrackingPointer); 337 if (pointerIndex < 0) { 338 pointerIndex = 0; 339 mTrackingPointer = event.getPointerId(pointerIndex); 340 } 341 final float x = event.getX(pointerIndex); 342 final float y = event.getY(pointerIndex); 343 344 switch (event.getActionMasked()) { 345 case MotionEvent.ACTION_DOWN: 346 mIntercepting = true; 347 mInitialTouchY = y; 348 mInitialTouchX = x; 349 initVelocityTracker(); 350 trackMovement(event); 351 if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { 352 getParent().requestDisallowInterceptTouchEvent(true); 353 } 354 break; 355 case MotionEvent.ACTION_POINTER_UP: 356 final int upPointer = event.getPointerId(event.getActionIndex()); 357 if (mTrackingPointer == upPointer) { 358 // gesture is ongoing, find a new pointer to track 359 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 360 mTrackingPointer = event.getPointerId(newIndex); 361 mInitialTouchX = event.getX(newIndex); 362 mInitialTouchY = event.getY(newIndex); 363 } 364 break; 365 366 case MotionEvent.ACTION_MOVE: 367 final float h = y - mInitialTouchY; 368 trackMovement(event); 369 if (mQsTracking) { 370 371 // Already tracking because onOverscrolled was called. We need to update here 372 // so we don't stop for a frame until the next touch event gets handled in 373 // onTouchEvent. 374 setQsExpansion(h + mInitialHeightOnTouch); 375 trackMovement(event); 376 mIntercepting = false; 377 return true; 378 } 379 if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX) 380 && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { 381 onQsExpansionStarted(); 382 mInitialHeightOnTouch = mQsExpansionHeight; 383 mInitialTouchY = y; 384 mInitialTouchX = x; 385 mQsTracking = true; 386 mIntercepting = false; 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 private void onQsExpansionStarted() { 527 onQsExpansionStarted(0); 528 } 529 530 private void onQsExpansionStarted(int overscrollAmount) { 531 cancelAnimation(); 532 533 // Reset scroll position and apply that position to the expanded height. 534 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 535 mScrollView.scrollTo(0, 0); 536 setQsExpansion(height); 537 } 538 539 private void setQsExpanded(boolean expanded) { 540 boolean changed = mQsExpanded != expanded; 541 if (changed) { 542 mQsExpanded = expanded; 543 updateQsState(); 544 } 545 } 546 547 public void setKeyguardShowing(boolean keyguardShowing) { 548 mKeyguardShowing = keyguardShowing; 549 updateQsState(); 550 } 551 552 private void updateQsState() { 553 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; 554 mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling); 555 mNotificationStackScroller.setEnabled(!mQsExpanded); 556 mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 557 mQsContainer.setVisibility(mKeyguardShowing && !mQsExpanded 558 ? View.INVISIBLE 559 : View.VISIBLE); 560 mScrollView.setTouchEnabled(mQsExpanded); 561 } 562 563 private void setQsExpansion(float height) { 564 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); 565 mQsFullyExpanded = height == mQsMaxExpansionHeight; 566 if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) { 567 setQsExpanded(true); 568 } else if (height <= mQsMinExpansionHeight && mQsExpanded) { 569 setQsExpanded(false); 570 } 571 mQsExpansionHeight = height; 572 mHeader.setExpansion(height - mQsPeekHeight); 573 setQsTranslation(height); 574 if (!mStackScrollerOverscrolling) { 575 setQsStackScrollerPadding(height); 576 } 577 mStatusBar.userActivity(); 578 } 579 580 private void setQsTranslation(float height) { 581 mQsContainer.setY(height - mQsContainer.getHeight()); 582 } 583 584 private void setQsStackScrollerPadding(float height) { 585 float start = height - mScrollView.getScrollY() + mNotificationTopPadding; 586 float stackHeight = mNotificationStackScroller.getHeight() - start; 587 if (stackHeight <= mMinStackHeight) { 588 float overflow = mMinStackHeight - stackHeight; 589 stackHeight = mMinStackHeight; 590 start = mNotificationStackScroller.getHeight() - stackHeight; 591 mNotificationStackScroller.setTranslationY(overflow); 592 mNotificationTranslation = overflow + mScrollView.getScrollY(); 593 } else { 594 mNotificationStackScroller.setTranslationY(0); 595 mNotificationTranslation = mScrollView.getScrollY(); 596 } 597 mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false); 598 } 599 600 private int clampQsStackScrollerPadding(int desiredPadding) { 601 return Math.max(desiredPadding, mStackScrollerIntrinsicPadding); 602 } 603 604 private void trackMovement(MotionEvent event) { 605 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 606 mLastTouchX = event.getX(); 607 mLastTouchY = event.getY(); 608 } 609 610 private void initVelocityTracker() { 611 if (mVelocityTracker != null) { 612 mVelocityTracker.recycle(); 613 } 614 mVelocityTracker = VelocityTracker.obtain(); 615 } 616 617 private float getCurrentVelocity() { 618 if (mVelocityTracker == null) { 619 return 0; 620 } 621 mVelocityTracker.computeCurrentVelocity(1000); 622 return mVelocityTracker.getYVelocity(); 623 } 624 625 private void cancelAnimation() { 626 if (mQsExpansionAnimator != null) { 627 mQsExpansionAnimator.cancel(); 628 } 629 } 630 private void flingSettings(float vel, boolean expand) { 631 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 632 if (target == mQsExpansionHeight) { 633 return; 634 } 635 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 636 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 637 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 638 @Override 639 public void onAnimationUpdate(ValueAnimator animation) { 640 setQsExpansion((Float) animation.getAnimatedValue()); 641 } 642 }); 643 animator.addListener(new AnimatorListenerAdapter() { 644 @Override 645 public void onAnimationEnd(Animator animation) { 646 mQsExpansionAnimator = null; 647 } 648 }); 649 animator.start(); 650 mQsExpansionAnimator = animator; 651 } 652 653 /** 654 * @return Whether we should intercept a gesture to open Quick Settings. 655 */ 656 private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { 657 if (!mQsExpansionEnabled) { 658 return false; 659 } 660 boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight() 661 && y >= mHeader.getTop() && y <= mHeader.getBottom(); 662 if (mQsExpanded) { 663 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0); 664 } else { 665 return onHeader; 666 } 667 } 668 669 @Override 670 public void setVisibility(int visibility) { 671 int oldVisibility = getVisibility(); 672 super.setVisibility(visibility); 673 if (visibility != oldVisibility) { 674 reparentStatusIcons(visibility == VISIBLE); 675 } 676 } 677 678 /** 679 * When the notification panel gets expanded, we need to move the status icons in the header 680 * card. 681 */ 682 private void reparentStatusIcons(boolean toHeader) { 683 if (mStatusBar == null) { 684 return; 685 } 686 LinearLayout systemIcons = mStatusBar.getSystemIcons(); 687 if (systemIcons.getParent() != null) { 688 ((ViewGroup) systemIcons.getParent()).removeView(systemIcons); 689 } 690 if (toHeader) { 691 mHeader.attachSystemIcons(systemIcons); 692 } else { 693 mHeader.onSystemIconsDetached(); 694 mStatusBar.reattachSystemIcons(); 695 } 696 } 697 698 @Override 699 protected boolean isScrolledToBottom() { 700 if (!isInSettings()) { 701 return mNotificationStackScroller.isScrolledToBottom(); 702 } 703 return super.isScrolledToBottom(); 704 } 705 706 @Override 707 protected int getMaxPanelHeight() { 708 // TODO: Figure out transition for collapsing when QS is open, adjust height here. 709 int maxPanelHeight = super.getMaxPanelHeight(); 710 int emptyBottomMargin = mStackScrollerContainer.getHeight() 711 - mNotificationStackScroller.getHeight() 712 + mNotificationStackScroller.getEmptyBottomMargin(); 713 int maxHeight = maxPanelHeight - emptyBottomMargin - mTopPaddingAdjustment; 714 maxHeight = Math.max(maxHeight, mStatusBarMinHeight); 715 return maxHeight; 716 } 717 718 private boolean isInSettings() { 719 return mQsExpanded; 720 } 721 722 @Override 723 protected void onHeightUpdated(float expandedHeight) { 724 if (!mQsExpanded) { 725 positionClockAndNotifications(); 726 } 727 mNotificationStackScroller.setStackHeight(expandedHeight); 728 updateKeyguardHeaderVisibility(); 729 updateUnlockIcon(); 730 } 731 732 private void updateUnlockIcon() { 733 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 734 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 735 boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance; 736 if (active && !mUnlockIconActive && mTracking) { 737 mKeyguardBottomArea.getLockIcon().animate() 738 .alpha(1f) 739 .scaleY(LOCK_ICON_ACTIVE_SCALE) 740 .scaleX(LOCK_ICON_ACTIVE_SCALE) 741 .setInterpolator(mFastOutLinearInterpolator) 742 .setDuration(150); 743 } else if (!active && mUnlockIconActive && mTracking) { 744 mKeyguardBottomArea.getLockIcon().animate() 745 .alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT) 746 .scaleY(1f) 747 .scaleX(1f) 748 .setInterpolator(mFastOutLinearInterpolator) 749 .setDuration(150); 750 } 751 mUnlockIconActive = active; 752 } 753 } 754 755 /** 756 * Hides the header when notifications are colliding with it. 757 */ 758 private void updateKeyguardHeaderVisibility() { 759 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 760 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 761 boolean hidden = mNotificationStackScroller.getNotificationsTopY() 762 <= mHeader.getBottom() + mNotificationsHeaderCollideDistance; 763 if (hidden && !mHeaderHidden) { 764 mHeader.animate() 765 .alpha(0f) 766 .withLayer() 767 .translationY(-mHeader.getHeight()/2) 768 .setInterpolator(mFastOutLinearInterpolator) 769 .setDuration(200); 770 } else if (!hidden && mHeaderHidden) { 771 mHeader.animate() 772 .alpha(1f) 773 .withLayer() 774 .translationY(0) 775 .setInterpolator(mLinearOutSlowInInterpolator) 776 .setDuration(200); 777 } 778 mHeaderHidden = hidden; 779 } else { 780 mHeader.animate().cancel(); 781 mHeader.setAlpha(1f); 782 mHeader.setTranslationY(0f); 783 if (mHeader.getLayerType() != LAYER_TYPE_NONE) { 784 mHeader.setLayerType(LAYER_TYPE_NONE, null); 785 } 786 mHeaderHidden = false; 787 } 788 789 } 790 791 @Override 792 protected void onExpandingStarted() { 793 super.onExpandingStarted(); 794 mNotificationStackScroller.onExpansionStarted(); 795 mIsExpanding = true; 796 } 797 798 @Override 799 protected void onExpandingFinished() { 800 super.onExpandingFinished(); 801 mNotificationStackScroller.onExpansionStopped(); 802 mIsExpanding = false; 803 } 804 805 @Override 806 protected void onOverExpansionChanged(float overExpansion) { 807 float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true); 808 float expansionChange = overExpansion - mOverExpansion; 809 expansionChange *= EXPANSION_RUBBER_BAND_EXTRA_FACTOR; 810 mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + expansionChange, 811 true /* onTop */, 812 false /* animate */); 813 super.onOverExpansionChanged(overExpansion); 814 } 815 816 @Override 817 protected void onTrackingStarted() { 818 super.onTrackingStarted(); 819 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 820 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 821 mPageSwiper.animateHideLeftRightIcon(); 822 } 823 } 824 825 @Override 826 protected void onTrackingStopped(boolean expand) { 827 super.onTrackingStopped(expand); 828 mOverExpansion = 0.0f; 829 mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */, true /* animate */); 830 if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 831 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 832 mPageSwiper.showAllIcons(true); 833 } 834 if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 835 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 836 mKeyguardBottomArea.getLockIcon().animate() 837 .alpha(0f) 838 .scaleX(2f) 839 .scaleY(2f) 840 .setInterpolator(mFastOutLinearInterpolator) 841 .setDuration(100); 842 } 843 } 844 845 @Override 846 public void onHeightChanged(ExpandableView view) { 847 requestPanelHeightUpdate(); 848 } 849 850 @Override 851 public void onScrollChanged() { 852 if (mQsExpanded) { 853 mNotificationStackScroller.setTranslationY( 854 mNotificationTranslation - mScrollView.getScrollY()); 855 } 856 } 857 858 @Override 859 protected void onConfigurationChanged(Configuration newConfig) { 860 super.onConfigurationChanged(newConfig); 861 mPageSwiper.onConfigurationChanged(); 862 } 863 864 @Override 865 public void onClick(View v) { 866 if (v == mHeader.getBackgroundView()) { 867 onQsExpansionStarted(); 868 if (mQsExpanded) { 869 flingSettings(0 /* vel */, false /* expand */); 870 } else if (mQsExpansionEnabled) { 871 flingSettings(0 /* vel */, true /* expand */); 872 } 873 } 874 } 875 876 @Override 877 public void onAnimationToSideStarted(boolean rightPage) { 878 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; 879 if (start) { 880 mKeyguardBottomArea.launchPhone(); 881 } else { 882 mKeyguardBottomArea.launchCamera(); 883 } 884 mBlockTouches = true; 885 } 886 887 @Override 888 protected void onEdgeClicked(boolean right) { 889 if ((right && getRightIcon().getVisibility() != View.VISIBLE) 890 || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) { 891 return; 892 } 893 mHintAnimationRunning = true; 894 mPageSwiper.startHintAnimation(right, new Runnable() { 895 @Override 896 public void run() { 897 mHintAnimationRunning = false; 898 mStatusBar.onHintFinished(); 899 } 900 }); 901 startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon()); 902 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; 903 if (start) { 904 mStatusBar.onPhoneHintStarted(); 905 } else { 906 mStatusBar.onCameraHintStarted(); 907 } 908 } 909 910 @Override 911 protected void startUnlockHintAnimation() { 912 super.startUnlockHintAnimation(); 913 startHighlightIconAnimation(getCenterIcon()); 914 } 915 916 /** 917 * Starts the highlight (making it fully opaque) animation on an icon. 918 */ 919 private void startHighlightIconAnimation(final View icon) { 920 icon.animate() 921 .alpha(1.0f) 922 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 923 .setInterpolator(mFastOutSlowInInterpolator) 924 .withEndAction(new Runnable() { 925 @Override 926 public void run() { 927 icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT) 928 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 929 .setInterpolator(mFastOutSlowInInterpolator); 930 } 931 }); 932 } 933 934 @Override 935 public float getPageWidth() { 936 return getWidth(); 937 } 938 939 @Override 940 public ArrayList<View> getTranslationViews() { 941 return mSwipeTranslationViews; 942 } 943 944 @Override 945 public View getLeftIcon() { 946 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 947 ? mKeyguardBottomArea.getCameraImageView() 948 : mKeyguardBottomArea.getPhoneImageView(); 949 } 950 951 @Override 952 public View getCenterIcon() { 953 return mKeyguardBottomArea.getLockIcon(); 954 } 955 956 @Override 957 public View getRightIcon() { 958 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 959 ? mKeyguardBottomArea.getPhoneImageView() 960 : mKeyguardBottomArea.getCameraImageView(); 961 } 962} 963