NotificationPanelView.java revision 93a2bb2063fdbca9f844202f0eca12330605511b
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.util.Log; 27import android.view.MotionEvent; 28import android.view.VelocityTracker; 29import android.view.View; 30import android.view.ViewGroup; 31import android.view.ViewTreeObserver; 32import android.view.accessibility.AccessibilityEvent; 33import android.view.animation.AnimationUtils; 34import android.view.animation.Interpolator; 35import android.view.animation.LinearInterpolator; 36import android.widget.LinearLayout; 37 38import com.android.systemui.R; 39import com.android.systemui.statusbar.ExpandableView; 40import com.android.systemui.statusbar.FlingAnimationUtils; 41import com.android.systemui.statusbar.GestureRecorder; 42import com.android.systemui.statusbar.StatusBarState; 43import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 44import com.android.systemui.statusbar.stack.StackStateAnimator; 45 46import java.util.ArrayList; 47 48public class NotificationPanelView extends PanelView implements 49 ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, 50 View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener, 51 KeyguardPageSwipeHelper.Callback { 52 53 private static float EXPANSION_RUBBER_BAND_EXTRA_FACTOR = 0.6f; 54 55 private KeyguardPageSwipeHelper mPageSwiper; 56 private StatusBarHeaderView mHeader; 57 private View mQsContainer; 58 private View mQsPanel; 59 private View mKeyguardStatusView; 60 private ObservableScrollView mScrollView; 61 private View mStackScrollerContainer; 62 63 private NotificationStackScrollLayout mNotificationStackScroller; 64 private int mNotificationTopPadding; 65 private boolean mAnimateNextTopPaddingChange; 66 67 private int mTrackingPointer; 68 private VelocityTracker mVelocityTracker; 69 private boolean mQsTracking; 70 71 /** 72 * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't 73 * intercepted yet. 74 */ 75 private boolean mIntercepting; 76 private boolean mQsExpanded; 77 private boolean mQsFullyExpanded; 78 private boolean mKeyguardShowing; 79 private float mInitialHeightOnTouch; 80 private float mInitialTouchX; 81 private float mInitialTouchY; 82 private float mLastTouchX; 83 private float mLastTouchY; 84 private float mQsExpansionHeight; 85 private int mQsMinExpansionHeight; 86 private int mQsMaxExpansionHeight; 87 private int mMinStackHeight; 88 private int mQsPeekHeight; 89 private float mNotificationTranslation; 90 private int mStackScrollerIntrinsicPadding; 91 private boolean mStackScrollerOverscrolling; 92 private boolean mQsExpansionEnabled = true; 93 private ValueAnimator mQsExpansionAnimator; 94 private FlingAnimationUtils mFlingAnimationUtils; 95 private int mStatusBarMinHeight; 96 private boolean mHeaderHidden; 97 private int mNotificationsHeaderCollideDistance; 98 99 private Interpolator mFastOutSlowInInterpolator; 100 private Interpolator mFastOutLinearInterpolator; 101 private Interpolator mLinearOutSlowInInterpolator; 102 private ObjectAnimator mClockAnimator; 103 private int mClockAnimationTarget = -1; 104 private int mTopPaddingAdjustment; 105 private KeyguardClockPositionAlgorithm mClockPositionAlgorithm = 106 new KeyguardClockPositionAlgorithm(); 107 private KeyguardClockPositionAlgorithm.Result mClockPositionResult = 108 new KeyguardClockPositionAlgorithm.Result(); 109 private boolean mIsSwipedHorizontally; 110 private boolean mIsExpanding; 111 private KeyguardBottomAreaView mKeyguardBottomArea; 112 private boolean mBlockTouches; 113 private ArrayList<View> mSwipeTranslationViews = new ArrayList<>(); 114 115 public NotificationPanelView(Context context, AttributeSet attrs) { 116 super(context, attrs); 117 } 118 119 public void setStatusBar(PhoneStatusBar bar) { 120 if (mStatusBar != null) { 121 mStatusBar.setOnFlipRunnable(null); 122 } 123 mStatusBar = bar; 124 if (bar != null) { 125 mStatusBar.setOnFlipRunnable(new Runnable() { 126 @Override 127 public void run() { 128 requestPanelHeightUpdate(); 129 } 130 }); 131 } 132 } 133 134 @Override 135 protected void onFinishInflate() { 136 super.onFinishInflate(); 137 mHeader = (StatusBarHeaderView) findViewById(R.id.header); 138 mHeader.getBackgroundView().setOnClickListener(this); 139 mHeader.setOverlayParent(this); 140 mKeyguardStatusView = findViewById(R.id.keyguard_status_view); 141 mStackScrollerContainer = findViewById(R.id.notification_container_parent); 142 mQsContainer = findViewById(R.id.quick_settings_container); 143 mQsPanel = findViewById(R.id.quick_settings_panel); 144 mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view); 145 mScrollView.setListener(this); 146 mNotificationStackScroller = (NotificationStackScrollLayout) 147 findViewById(R.id.notification_stack_scroller); 148 mNotificationStackScroller.setOnHeightChangedListener(this); 149 mNotificationStackScroller.setOverscrollTopChangedListener(this); 150 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), 151 android.R.interpolator.fast_out_slow_in); 152 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), 153 android.R.interpolator.linear_out_slow_in); 154 mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(), 155 android.R.interpolator.fast_out_linear_in); 156 mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area); 157 mSwipeTranslationViews.add(mNotificationStackScroller); 158 mSwipeTranslationViews.add(mKeyguardStatusView); 159 mPageSwiper = new KeyguardPageSwipeHelper(this, getContext()); 160 } 161 162 @Override 163 protected void loadDimens() { 164 super.loadDimens(); 165 mNotificationTopPadding = getResources().getDimensionPixelSize( 166 R.dimen.notifications_top_padding); 167 mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height); 168 mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f); 169 mStatusBarMinHeight = getResources().getDimensionPixelSize( 170 com.android.internal.R.dimen.status_bar_height); 171 mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height); 172 mNotificationsHeaderCollideDistance = 173 getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance); 174 mClockPositionAlgorithm.loadDimens(getResources()); 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 setQsStackScrollerPadding(mQsMaxExpansionHeight); 187 } 188 } else { 189 if (!mStackScrollerOverscrolling) { 190 setQsExpansion(mQsMinExpansionHeight); 191 } 192 positionClockAndNotifications(); 193 mNotificationStackScroller.setStackHeight(getExpandedHeight()); 194 } 195 } 196 197 /** 198 * Positions the clock and notifications dynamically depending on how many notifications are 199 * showing. 200 */ 201 private void positionClockAndNotifications() { 202 boolean animateClock = mNotificationStackScroller.isAddOrRemoveAnimationPending(); 203 if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) { 204 int bottom = mStackScrollerOverscrolling 205 ? mHeader.getCollapsedHeight() 206 : mHeader.getBottom(); 207 mStackScrollerIntrinsicPadding = 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 mStackScrollerIntrinsicPadding = mClockPositionResult.stackScrollerPadding; 226 mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment; 227 } 228 mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding, 229 mAnimateNextTopPaddingChange || animateClock); 230 mAnimateNextTopPaddingChange = false; 231 } 232 233 private void startClockAnimation(int y) { 234 if (mClockAnimationTarget == y) { 235 return; 236 } 237 mClockAnimationTarget = y; 238 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 239 @Override 240 public boolean onPreDraw() { 241 getViewTreeObserver().removeOnPreDrawListener(this); 242 if (mClockAnimator != null) { 243 mClockAnimator.removeAllListeners(); 244 mClockAnimator.cancel(); 245 } 246 mClockAnimator = 247 ObjectAnimator.ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget); 248 mClockAnimator.setInterpolator(mFastOutSlowInInterpolator); 249 mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 250 mClockAnimator.addListener(new AnimatorListenerAdapter() { 251 @Override 252 public void onAnimationEnd(Animator animation) { 253 mClockAnimator = null; 254 mClockAnimationTarget = -1; 255 } 256 }); 257 mClockAnimator.start(); 258 return true; 259 } 260 }); 261 } 262 263 private void applyClockAlpha(float alpha) { 264 if (alpha != 1.0f) { 265 mKeyguardStatusView.setLayerType(LAYER_TYPE_HARDWARE, null); 266 } else { 267 mKeyguardStatusView.setLayerType(LAYER_TYPE_NONE, null); 268 } 269 mKeyguardStatusView.setAlpha(alpha); 270 } 271 272 public void animateToFullShade() { 273 mAnimateNextTopPaddingChange = true; 274 mNotificationStackScroller.goToFullShade(); 275 requestLayout(); 276 } 277 278 /** 279 * @return Whether Quick Settings are currently expanded. 280 */ 281 public boolean isQsExpanded() { 282 return mQsExpanded; 283 } 284 285 public void setQsExpansionEnabled(boolean qsExpansionEnabled) { 286 mQsExpansionEnabled = qsExpansionEnabled; 287 } 288 289 @Override 290 public void resetViews() { 291 mBlockTouches = false; 292 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 return true; 385 } 386 break; 387 388 case MotionEvent.ACTION_CANCEL: 389 case MotionEvent.ACTION_UP: 390 trackMovement(event); 391 if (mQsTracking) { 392 flingQsWithCurrentVelocity(); 393 mQsTracking = false; 394 } 395 mIntercepting = false; 396 break; 397 } 398 return !mQsExpanded && super.onInterceptTouchEvent(event); 399 } 400 401 @Override 402 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 403 404 // Block request when interacting with the scroll view so we can still intercept the 405 // scrolling when QS is expanded. 406 if (mScrollView.isDispatchingTouchEvent()) { 407 return; 408 } 409 super.requestDisallowInterceptTouchEvent(disallowIntercept); 410 } 411 412 private void flingQsWithCurrentVelocity() { 413 float vel = getCurrentVelocity(); 414 415 // TODO: Better logic whether we should expand or not. 416 flingSettings(vel, vel > 0); 417 } 418 419 @Override 420 public boolean onTouchEvent(MotionEvent event) { 421 if (mBlockTouches) { 422 return false; 423 } 424 // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference 425 // implementation. 426 if ((!mIsExpanding || mHintAnimationRunning) 427 && !mQsExpanded 428 && mStatusBar.getBarState() != StatusBarState.SHADE) { 429 mPageSwiper.onTouchEvent(event); 430 if (mPageSwiper.isSwipingInProgress()) { 431 return true; 432 } 433 } 434 if (mQsTracking || mQsExpanded) { 435 return onQsTouch(event); 436 } 437 438 super.onTouchEvent(event); 439 return true; 440 } 441 442 @Override 443 protected boolean hasConflictingGestures() { 444 return mStatusBar.getBarState() != StatusBarState.SHADE; 445 } 446 447 private boolean onQsTouch(MotionEvent event) { 448 int pointerIndex = event.findPointerIndex(mTrackingPointer); 449 if (pointerIndex < 0) { 450 pointerIndex = 0; 451 mTrackingPointer = event.getPointerId(pointerIndex); 452 } 453 final float y = event.getY(pointerIndex); 454 final float x = event.getX(pointerIndex); 455 456 switch (event.getActionMasked()) { 457 case MotionEvent.ACTION_DOWN: 458 mQsTracking = true; 459 mInitialTouchY = y; 460 mInitialTouchX = x; 461 onQsExpansionStarted(); 462 mInitialHeightOnTouch = mQsExpansionHeight; 463 initVelocityTracker(); 464 trackMovement(event); 465 break; 466 467 case MotionEvent.ACTION_POINTER_UP: 468 final int upPointer = event.getPointerId(event.getActionIndex()); 469 if (mTrackingPointer == upPointer) { 470 // gesture is ongoing, find a new pointer to track 471 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 472 final float newY = event.getY(newIndex); 473 final float newX = event.getX(newIndex); 474 mTrackingPointer = event.getPointerId(newIndex); 475 mInitialHeightOnTouch = mQsExpansionHeight; 476 mInitialTouchY = newY; 477 mInitialTouchX = newX; 478 } 479 break; 480 481 case MotionEvent.ACTION_MOVE: 482 final float h = y - mInitialTouchY; 483 setQsExpansion(h + mInitialHeightOnTouch); 484 trackMovement(event); 485 break; 486 487 case MotionEvent.ACTION_UP: 488 case MotionEvent.ACTION_CANCEL: 489 mQsTracking = false; 490 mTrackingPointer = -1; 491 trackMovement(event); 492 flingQsWithCurrentVelocity(); 493 if (mVelocityTracker != null) { 494 mVelocityTracker.recycle(); 495 mVelocityTracker = null; 496 } 497 break; 498 } 499 return true; 500 } 501 502 @Override 503 public void onOverscrolled(int amount) { 504 if (mIntercepting) { 505 onQsExpansionStarted(amount); 506 mInitialHeightOnTouch = mQsExpansionHeight; 507 mInitialTouchY = mLastTouchY; 508 mInitialTouchX = mLastTouchX; 509 mQsTracking = true; 510 } 511 } 512 513 514 @Override 515 public void onOverscrollTopChanged(float amount) { 516 cancelAnimation(); 517 float rounded = amount >= 1f ? amount : 0f; 518 mStackScrollerOverscrolling = rounded != 0f; 519 setQsExpansion(mQsMinExpansionHeight + rounded); 520 updateQsState(); 521 } 522 523 private void onQsExpansionStarted() { 524 onQsExpansionStarted(0); 525 } 526 527 private void onQsExpansionStarted(int overscrollAmount) { 528 cancelAnimation(); 529 530 // Reset scroll position and apply that position to the expanded height. 531 float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount; 532 mScrollView.scrollTo(0, 0); 533 setQsExpansion(height); 534 } 535 536 private void setQsExpanded(boolean expanded) { 537 boolean changed = mQsExpanded != expanded; 538 if (changed) { 539 mQsExpanded = expanded; 540 updateQsState(); 541 } 542 } 543 544 public void setKeyguardShowing(boolean keyguardShowing) { 545 mKeyguardShowing = keyguardShowing; 546 updateQsState(); 547 } 548 549 private void updateQsState() { 550 boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling; 551 mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling); 552 mNotificationStackScroller.setEnabled(!mQsExpanded); 553 mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE); 554 mQsContainer.setVisibility(mKeyguardShowing && !mQsExpanded 555 ? View.INVISIBLE 556 : View.VISIBLE); 557 mScrollView.setTouchEnabled(mQsExpanded); 558 } 559 560 private void setQsExpansion(float height) { 561 height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight); 562 mQsFullyExpanded = height == mQsMaxExpansionHeight; 563 if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) { 564 setQsExpanded(true); 565 } else if (height <= mQsMinExpansionHeight && mQsExpanded) { 566 setQsExpanded(false); 567 } 568 mQsExpansionHeight = height; 569 mHeader.setExpansion(height - mQsPeekHeight); 570 setQsTranslation(height); 571 if (!mStackScrollerOverscrolling) { 572 setQsStackScrollerPadding(height); 573 } 574 mStatusBar.userActivity(); 575 } 576 577 private void setQsTranslation(float height) { 578 mQsContainer.setY(height - mQsContainer.getHeight()); 579 } 580 581 private void setQsStackScrollerPadding(float height) { 582 float start = height - mScrollView.getScrollY() + mNotificationTopPadding; 583 float stackHeight = mNotificationStackScroller.getHeight() - start; 584 if (stackHeight <= mMinStackHeight) { 585 float overflow = mMinStackHeight - stackHeight; 586 stackHeight = mMinStackHeight; 587 start = mNotificationStackScroller.getHeight() - stackHeight; 588 mNotificationStackScroller.setTranslationY(overflow); 589 mNotificationTranslation = overflow + mScrollView.getScrollY(); 590 } else { 591 mNotificationStackScroller.setTranslationY(0); 592 mNotificationTranslation = mScrollView.getScrollY(); 593 } 594 mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false); 595 } 596 597 private int clampQsStackScrollerPadding(int desiredPadding) { 598 return Math.max(desiredPadding, mStackScrollerIntrinsicPadding); 599 } 600 601 private void trackMovement(MotionEvent event) { 602 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 603 mLastTouchX = event.getX(); 604 mLastTouchY = event.getY(); 605 } 606 607 private void initVelocityTracker() { 608 if (mVelocityTracker != null) { 609 mVelocityTracker.recycle(); 610 } 611 mVelocityTracker = VelocityTracker.obtain(); 612 } 613 614 private float getCurrentVelocity() { 615 if (mVelocityTracker == null) { 616 return 0; 617 } 618 mVelocityTracker.computeCurrentVelocity(1000); 619 return mVelocityTracker.getYVelocity(); 620 } 621 622 private void cancelAnimation() { 623 if (mQsExpansionAnimator != null) { 624 mQsExpansionAnimator.cancel(); 625 } 626 } 627 private void flingSettings(float vel, boolean expand) { 628 float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight; 629 if (target == mQsExpansionHeight) { 630 return; 631 } 632 ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target); 633 mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel); 634 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 635 @Override 636 public void onAnimationUpdate(ValueAnimator animation) { 637 setQsExpansion((Float) animation.getAnimatedValue()); 638 } 639 }); 640 animator.addListener(new AnimatorListenerAdapter() { 641 @Override 642 public void onAnimationEnd(Animator animation) { 643 mQsExpansionAnimator = null; 644 } 645 }); 646 animator.start(); 647 mQsExpansionAnimator = animator; 648 } 649 650 /** 651 * @return Whether we should intercept a gesture to open Quick Settings. 652 */ 653 private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { 654 if (!mQsExpansionEnabled) { 655 return false; 656 } 657 boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight() 658 && y >= mHeader.getTop() && y <= mHeader.getBottom(); 659 if (mQsExpanded) { 660 return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0); 661 } else { 662 return onHeader; 663 } 664 } 665 666 @Override 667 public void setVisibility(int visibility) { 668 int oldVisibility = getVisibility(); 669 super.setVisibility(visibility); 670 if (visibility != oldVisibility) { 671 reparentStatusIcons(visibility == VISIBLE); 672 } 673 } 674 675 /** 676 * When the notification panel gets expanded, we need to move the status icons in the header 677 * card. 678 */ 679 private void reparentStatusIcons(boolean toHeader) { 680 if (mStatusBar == null) { 681 return; 682 } 683 LinearLayout systemIcons = mStatusBar.getSystemIcons(); 684 if (systemIcons.getParent() != null) { 685 ((ViewGroup) systemIcons.getParent()).removeView(systemIcons); 686 } 687 if (toHeader) { 688 mHeader.attachSystemIcons(systemIcons); 689 } else { 690 mHeader.onSystemIconsDetached(); 691 mStatusBar.reattachSystemIcons(); 692 } 693 } 694 695 @Override 696 protected boolean isScrolledToBottom() { 697 if (!isInSettings()) { 698 return mNotificationStackScroller.isScrolledToBottom(); 699 } 700 return super.isScrolledToBottom(); 701 } 702 703 @Override 704 protected int getMaxPanelHeight() { 705 // TODO: Figure out transition for collapsing when QS is open, adjust height here. 706 int maxPanelHeight = super.getMaxPanelHeight(); 707 int emptyBottomMargin = mStackScrollerContainer.getHeight() 708 - mNotificationStackScroller.getHeight() 709 + mNotificationStackScroller.getEmptyBottomMargin(); 710 int maxHeight = maxPanelHeight - emptyBottomMargin - mTopPaddingAdjustment; 711 maxHeight = Math.max(maxHeight, mStatusBarMinHeight); 712 return maxHeight; 713 } 714 715 private boolean isInSettings() { 716 return mQsExpanded; 717 } 718 719 @Override 720 protected void onHeightUpdated(float expandedHeight) { 721 if (!mQsExpanded) { 722 positionClockAndNotifications(); 723 } 724 mNotificationStackScroller.setStackHeight(expandedHeight); 725 updateKeyguardHeaderVisibility(); 726 } 727 728 /** 729 * Hides the header when notifications are colliding with it. 730 */ 731 private void updateKeyguardHeaderVisibility() { 732 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 733 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 734 boolean hidden = mNotificationStackScroller.getNotificationsTopY() 735 <= mHeader.getBottom() + mNotificationsHeaderCollideDistance; 736 if (hidden && !mHeaderHidden) { 737 mHeader.animate() 738 .alpha(0f) 739 .withLayer() 740 .translationY(-mHeader.getHeight()/2) 741 .setInterpolator(mFastOutLinearInterpolator) 742 .setDuration(200); 743 } else if (!hidden && mHeaderHidden) { 744 mHeader.animate() 745 .alpha(1f) 746 .withLayer() 747 .translationY(0) 748 .setInterpolator(mLinearOutSlowInInterpolator) 749 .setDuration(200); 750 } 751 mHeaderHidden = hidden; 752 } else { 753 mHeader.animate().cancel(); 754 mHeader.setAlpha(1f); 755 mHeader.setTranslationY(0f); 756 if (mHeader.getLayerType() != LAYER_TYPE_NONE) { 757 mHeader.setLayerType(LAYER_TYPE_NONE, null); 758 } 759 mHeaderHidden = false; 760 } 761 762 } 763 764 @Override 765 protected void onExpandingStarted() { 766 super.onExpandingStarted(); 767 mNotificationStackScroller.onExpansionStarted(); 768 mIsExpanding = true; 769 } 770 771 @Override 772 protected void onExpandingFinished() { 773 super.onExpandingFinished(); 774 mNotificationStackScroller.onExpansionStopped(); 775 mIsExpanding = false; 776 } 777 778 @Override 779 protected void onOverExpansionChanged(float overExpansion) { 780 float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true); 781 float expansionChange = overExpansion - mOverExpansion; 782 expansionChange *= EXPANSION_RUBBER_BAND_EXTRA_FACTOR; 783 mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + expansionChange, 784 true /* onTop */, 785 false /* animate */); 786 super.onOverExpansionChanged(overExpansion); 787 } 788 789 @Override 790 protected void onTrackingStarted() { 791 super.onTrackingStarted(); 792 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD 793 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) { 794 mPageSwiper.animateHideLeftRightIcon(); 795 } 796 } 797 798 @Override 799 protected void onTrackingStopped(boolean expand) { 800 super.onTrackingStopped(expand); 801 mOverExpansion = 0.0f; 802 mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */, true /* animate */); 803 if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD 804 || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) { 805 mPageSwiper.showAllIcons(true); 806 } 807 } 808 809 @Override 810 public void onHeightChanged(ExpandableView view) { 811 requestPanelHeightUpdate(); 812 } 813 814 @Override 815 public void onScrollChanged() { 816 if (mQsExpanded) { 817 mNotificationStackScroller.setTranslationY( 818 mNotificationTranslation - mScrollView.getScrollY()); 819 } 820 } 821 822 @Override 823 protected void onConfigurationChanged(Configuration newConfig) { 824 super.onConfigurationChanged(newConfig); 825 mPageSwiper.onConfigurationChanged(); 826 } 827 828 @Override 829 public void onClick(View v) { 830 if (v == mHeader.getBackgroundView()) { 831 onQsExpansionStarted(); 832 if (mQsExpanded) { 833 flingSettings(0 /* vel */, false /* expand */); 834 } else if (mQsExpansionEnabled) { 835 flingSettings(0 /* vel */, true /* expand */); 836 } 837 } 838 } 839 840 @Override 841 public void onAnimationToSideStarted(boolean rightPage) { 842 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage; 843 if (start) { 844 mKeyguardBottomArea.launchPhone(); 845 } else { 846 mKeyguardBottomArea.launchCamera(); 847 } 848 mBlockTouches = true; 849 } 850 851 @Override 852 protected void onEdgeClicked(boolean right) { 853 if ((right && getRightIcon().getVisibility() != View.VISIBLE) 854 || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) { 855 return; 856 } 857 mHintAnimationRunning = true; 858 mPageSwiper.startHintAnimation(right, new Runnable() { 859 @Override 860 public void run() { 861 mHintAnimationRunning = false; 862 mStatusBar.onHintFinished(); 863 } 864 }); 865 startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon()); 866 boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right; 867 if (start) { 868 mStatusBar.onPhoneHintStarted(); 869 } else { 870 mStatusBar.onCameraHintStarted(); 871 } 872 } 873 874 @Override 875 protected void startUnlockHintAnimation() { 876 super.startUnlockHintAnimation(); 877 startHighlightIconAnimation(getCenterIcon()); 878 } 879 880 /** 881 * Starts the highlight (making it fully opaque) animation on an icon. 882 */ 883 private void startHighlightIconAnimation(final View icon) { 884 icon.animate() 885 .alpha(1.0f) 886 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 887 .setInterpolator(mFastOutSlowInInterpolator) 888 .withEndAction(new Runnable() { 889 @Override 890 public void run() { 891 icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT) 892 .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION) 893 .setInterpolator(mFastOutSlowInInterpolator); 894 } 895 }); 896 } 897 898 @Override 899 public float getPageWidth() { 900 return getWidth(); 901 } 902 903 @Override 904 public ArrayList<View> getTranslationViews() { 905 return mSwipeTranslationViews; 906 } 907 908 @Override 909 public View getLeftIcon() { 910 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 911 ? mKeyguardBottomArea.getCameraImageView() 912 : mKeyguardBottomArea.getPhoneImageView(); 913 } 914 915 @Override 916 public View getCenterIcon() { 917 return mKeyguardBottomArea.getLockIcon(); 918 } 919 920 @Override 921 public View getRightIcon() { 922 return getLayoutDirection() == LAYOUT_DIRECTION_RTL 923 ? mKeyguardBottomArea.getPhoneImageView() 924 : mKeyguardBottomArea.getCameraImageView(); 925 } 926} 927