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 17 package com.android.systemui.statusbar.phone; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.content.res.Configuration; 25 import android.content.res.Resources; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.view.InputDevice; 29 import android.view.MotionEvent; 30 import android.view.ViewConfiguration; 31 import android.view.ViewTreeObserver; 32 import android.view.animation.Interpolator; 33 import android.widget.FrameLayout; 34 35 import com.android.systemui.EventLogConstants; 36 import com.android.systemui.EventLogTags; 37 import com.android.systemui.Interpolators; 38 import com.android.systemui.R; 39 import com.android.systemui.classifier.FalsingManager; 40 import com.android.systemui.doze.DozeLog; 41 import com.android.systemui.statusbar.FlingAnimationUtils; 42 import com.android.systemui.statusbar.StatusBarState; 43 import com.android.systemui.statusbar.policy.HeadsUpManager; 44 45 import java.io.FileDescriptor; 46 import java.io.PrintWriter; 47 48 public abstract class PanelView extends FrameLayout { 49 public static final boolean DEBUG = PanelBar.DEBUG; 50 public static final String TAG = PanelView.class.getSimpleName(); 51 52 private final void logf(String fmt, Object... args) { 53 Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); 54 } 55 56 protected PhoneStatusBar mStatusBar; 57 protected HeadsUpManager mHeadsUpManager; 58 59 private float mPeekHeight; 60 private float mHintDistance; 61 private float mInitialOffsetOnTouch; 62 private boolean mCollapsedAndHeadsUpOnDown; 63 private float mExpandedFraction = 0; 64 protected float mExpandedHeight = 0; 65 private boolean mPanelClosedOnDown; 66 private boolean mHasLayoutedSinceDown; 67 private float mUpdateFlingVelocity; 68 private boolean mUpdateFlingOnLayout; 69 private boolean mPeekTouching; 70 private boolean mJustPeeked; 71 private boolean mClosing; 72 protected boolean mTracking; 73 private boolean mTouchSlopExceeded; 74 private int mTrackingPointer; 75 protected int mTouchSlop; 76 protected boolean mHintAnimationRunning; 77 private boolean mOverExpandedBeforeFling; 78 private boolean mTouchAboveFalsingThreshold; 79 private int mUnlockFalsingThreshold; 80 private boolean mTouchStartedInEmptyArea; 81 private boolean mMotionAborted; 82 private boolean mUpwardsWhenTresholdReached; 83 private boolean mAnimatingOnDown; 84 85 private ValueAnimator mHeightAnimator; 86 private ObjectAnimator mPeekAnimator; 87 private VelocityTrackerInterface mVelocityTracker; 88 private FlingAnimationUtils mFlingAnimationUtils; 89 private FalsingManager mFalsingManager; 90 91 /** 92 * Whether an instant expand request is currently pending and we are just waiting for layout. 93 */ 94 private boolean mInstantExpanding; 95 96 PanelBar mBar; 97 98 private String mViewName; 99 private float mInitialTouchY; 100 private float mInitialTouchX; 101 private boolean mTouchDisabled; 102 103 private Interpolator mBounceInterpolator; 104 protected KeyguardBottomAreaView mKeyguardBottomArea; 105 106 private boolean mPeekPending; 107 private boolean mCollapseAfterPeek; 108 109 /** 110 * Speed-up factor to be used when {@link #mFlingCollapseRunnable} runs the next time. 111 */ 112 private float mNextCollapseSpeedUpFactor = 1.0f; 113 114 protected boolean mExpanding; 115 private boolean mGestureWaitForTouchSlop; 116 private boolean mIgnoreXTouchSlop; 117 private Runnable mPeekRunnable = new Runnable() { 118 @Override 119 public void run() { 120 mPeekPending = false; 121 runPeekAnimation(); 122 } 123 }; 124 125 protected void onExpandingFinished() { 126 mBar.onExpandingFinished(); 127 } 128 129 protected void onExpandingStarted() { 130 } 131 132 private void notifyExpandingStarted() { 133 if (!mExpanding) { 134 mExpanding = true; 135 onExpandingStarted(); 136 } 137 } 138 139 protected final void notifyExpandingFinished() { 140 endClosing(); 141 if (mExpanding) { 142 mExpanding = false; 143 onExpandingFinished(); 144 } 145 } 146 147 private void schedulePeek() { 148 mPeekPending = true; 149 long timeout = ViewConfiguration.getTapTimeout(); 150 postOnAnimationDelayed(mPeekRunnable, timeout); 151 notifyBarPanelExpansionChanged(); 152 } 153 154 private void runPeekAnimation() { 155 mPeekHeight = getPeekHeight(); 156 if (DEBUG) logf("peek to height=%.1f", mPeekHeight); 157 if (mHeightAnimator != null) { 158 return; 159 } 160 mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight) 161 .setDuration(250); 162 mPeekAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 163 mPeekAnimator.addListener(new AnimatorListenerAdapter() { 164 private boolean mCancelled; 165 166 @Override 167 public void onAnimationCancel(Animator animation) { 168 mCancelled = true; 169 } 170 171 @Override 172 public void onAnimationEnd(Animator animation) { 173 mPeekAnimator = null; 174 if (mCollapseAfterPeek && !mCancelled) { 175 postOnAnimation(mPostCollapseRunnable); 176 } 177 mCollapseAfterPeek = false; 178 } 179 }); 180 notifyExpandingStarted(); 181 mPeekAnimator.start(); 182 mJustPeeked = true; 183 } 184 185 public PanelView(Context context, AttributeSet attrs) { 186 super(context, attrs); 187 mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f); 188 mBounceInterpolator = new BounceInterpolator(); 189 mFalsingManager = FalsingManager.getInstance(context); 190 } 191 192 protected void loadDimens() { 193 final Resources res = getContext().getResources(); 194 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 195 mTouchSlop = configuration.getScaledTouchSlop(); 196 mHintDistance = res.getDimension(R.dimen.hint_move_distance); 197 mUnlockFalsingThreshold = res.getDimensionPixelSize(R.dimen.unlock_falsing_threshold); 198 } 199 200 private void trackMovement(MotionEvent event) { 201 // Add movement to velocity tracker using raw screen X and Y coordinates instead 202 // of window coordinates because the window frame may be moving at the same time. 203 float deltaX = event.getRawX() - event.getX(); 204 float deltaY = event.getRawY() - event.getY(); 205 event.offsetLocation(deltaX, deltaY); 206 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 207 event.offsetLocation(-deltaX, -deltaY); 208 } 209 210 public void setTouchDisabled(boolean disabled) { 211 mTouchDisabled = disabled; 212 if (mTouchDisabled && mTracking) { 213 onTrackingStopped(true /* expanded */); 214 } 215 } 216 217 @Override 218 public boolean onTouchEvent(MotionEvent event) { 219 if (mInstantExpanding || mTouchDisabled 220 || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { 221 return false; 222 } 223 224 // On expanding, single mouse click expands the panel instead of dragging. 225 if (isFullyCollapsed() && event.isFromSource(InputDevice.SOURCE_MOUSE)) { 226 if (event.getAction() == MotionEvent.ACTION_UP) { 227 expand(true); 228 } 229 return true; 230 } 231 232 /* 233 * We capture touch events here and update the expand height here in case according to 234 * the users fingers. This also handles multi-touch. 235 * 236 * If the user just clicks shortly, we show a quick peek of the shade. 237 * 238 * Flinging is also enabled in order to open or close the shade. 239 */ 240 241 int pointerIndex = event.findPointerIndex(mTrackingPointer); 242 if (pointerIndex < 0) { 243 pointerIndex = 0; 244 mTrackingPointer = event.getPointerId(pointerIndex); 245 } 246 final float x = event.getX(pointerIndex); 247 final float y = event.getY(pointerIndex); 248 249 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 250 mGestureWaitForTouchSlop = isFullyCollapsed() || hasConflictingGestures(); 251 mIgnoreXTouchSlop = isFullyCollapsed() || shouldGestureIgnoreXTouchSlop(x, y); 252 } 253 254 switch (event.getActionMasked()) { 255 case MotionEvent.ACTION_DOWN: 256 startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); 257 mJustPeeked = false; 258 mPanelClosedOnDown = isFullyCollapsed(); 259 mHasLayoutedSinceDown = false; 260 mUpdateFlingOnLayout = false; 261 mMotionAborted = false; 262 mPeekTouching = mPanelClosedOnDown; 263 mTouchAboveFalsingThreshold = false; 264 mCollapsedAndHeadsUpOnDown = isFullyCollapsed() 265 && mHeadsUpManager.hasPinnedHeadsUp(); 266 if (mVelocityTracker == null) { 267 initVelocityTracker(); 268 } 269 trackMovement(event); 270 if (!mGestureWaitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) || 271 mPeekPending || mPeekAnimator != null) { 272 cancelHeightAnimator(); 273 cancelPeek(); 274 mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning) 275 || mPeekPending || mPeekAnimator != null; 276 onTrackingStarted(); 277 } 278 if (isFullyCollapsed() && !mHeadsUpManager.hasPinnedHeadsUp()) { 279 schedulePeek(); 280 } 281 break; 282 283 case MotionEvent.ACTION_POINTER_UP: 284 final int upPointer = event.getPointerId(event.getActionIndex()); 285 if (mTrackingPointer == upPointer) { 286 // gesture is ongoing, find a new pointer to track 287 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 288 final float newY = event.getY(newIndex); 289 final float newX = event.getX(newIndex); 290 mTrackingPointer = event.getPointerId(newIndex); 291 startExpandMotion(newX, newY, true /* startTracking */, mExpandedHeight); 292 } 293 break; 294 case MotionEvent.ACTION_POINTER_DOWN: 295 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 296 mMotionAborted = true; 297 endMotionEvent(event, x, y, true /* forceCancel */); 298 return false; 299 } 300 break; 301 case MotionEvent.ACTION_MOVE: 302 float h = y - mInitialTouchY; 303 304 // If the panel was collapsed when touching, we only need to check for the 305 // y-component of the gesture, as we have no conflicting horizontal gesture. 306 if (Math.abs(h) > mTouchSlop 307 && (Math.abs(h) > Math.abs(x - mInitialTouchX) 308 || mIgnoreXTouchSlop)) { 309 mTouchSlopExceeded = true; 310 if (mGestureWaitForTouchSlop && !mTracking && !mCollapsedAndHeadsUpOnDown) { 311 if (!mJustPeeked && mInitialOffsetOnTouch != 0f) { 312 startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); 313 h = 0; 314 } 315 cancelHeightAnimator(); 316 removeCallbacks(mPeekRunnable); 317 mPeekPending = false; 318 onTrackingStarted(); 319 } 320 } 321 final float newHeight = Math.max(0, h + mInitialOffsetOnTouch); 322 if (newHeight > mPeekHeight) { 323 if (mPeekAnimator != null) { 324 mPeekAnimator.cancel(); 325 } 326 mJustPeeked = false; 327 } 328 if (-h >= getFalsingThreshold()) { 329 mTouchAboveFalsingThreshold = true; 330 mUpwardsWhenTresholdReached = isDirectionUpwards(x, y); 331 } 332 if (!mJustPeeked && (!mGestureWaitForTouchSlop || mTracking) && !isTrackingBlocked()) { 333 setExpandedHeightInternal(newHeight); 334 } 335 336 trackMovement(event); 337 break; 338 339 case MotionEvent.ACTION_UP: 340 case MotionEvent.ACTION_CANCEL: 341 trackMovement(event); 342 endMotionEvent(event, x, y, false /* forceCancel */); 343 break; 344 } 345 return !mGestureWaitForTouchSlop || mTracking; 346 } 347 348 /** 349 * @return whether the swiping direction is upwards and above a 45 degree angle compared to the 350 * horizontal direction 351 */ 352 private boolean isDirectionUpwards(float x, float y) { 353 float xDiff = x - mInitialTouchX; 354 float yDiff = y - mInitialTouchY; 355 if (yDiff >= 0) { 356 return false; 357 } 358 return Math.abs(yDiff) >= Math.abs(xDiff); 359 } 360 361 protected void startExpandMotion(float newX, float newY, boolean startTracking, 362 float expandedHeight) { 363 mInitialOffsetOnTouch = expandedHeight; 364 mInitialTouchY = newY; 365 mInitialTouchX = newX; 366 if (startTracking) { 367 mTouchSlopExceeded = true; 368 setExpandedHeight(mInitialOffsetOnTouch); 369 onTrackingStarted(); 370 } 371 } 372 373 private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) { 374 mTrackingPointer = -1; 375 if ((mTracking && mTouchSlopExceeded) 376 || Math.abs(x - mInitialTouchX) > mTouchSlop 377 || Math.abs(y - mInitialTouchY) > mTouchSlop 378 || event.getActionMasked() == MotionEvent.ACTION_CANCEL 379 || forceCancel) { 380 float vel = 0f; 381 float vectorVel = 0f; 382 if (mVelocityTracker != null) { 383 mVelocityTracker.computeCurrentVelocity(1000); 384 vel = mVelocityTracker.getYVelocity(); 385 vectorVel = (float) Math.hypot( 386 mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); 387 } 388 boolean expand = flingExpands(vel, vectorVel, x, y) 389 || event.getActionMasked() == MotionEvent.ACTION_CANCEL 390 || forceCancel; 391 DozeLog.traceFling(expand, mTouchAboveFalsingThreshold, 392 mStatusBar.isFalsingThresholdNeeded(), 393 mStatusBar.isWakeUpComingFromTouch()); 394 // Log collapse gesture if on lock screen. 395 if (!expand && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 396 float displayDensity = mStatusBar.getDisplayDensity(); 397 int heightDp = (int) Math.abs((y - mInitialTouchY) / displayDensity); 398 int velocityDp = (int) Math.abs(vel / displayDensity); 399 EventLogTags.writeSysuiLockscreenGesture( 400 EventLogConstants.SYSUI_LOCKSCREEN_GESTURE_SWIPE_UP_UNLOCK, 401 heightDp, velocityDp); 402 } 403 fling(vel, expand, isFalseTouch(x, y)); 404 onTrackingStopped(expand); 405 mUpdateFlingOnLayout = expand && mPanelClosedOnDown && !mHasLayoutedSinceDown; 406 if (mUpdateFlingOnLayout) { 407 mUpdateFlingVelocity = vel; 408 } 409 } else { 410 boolean expands = onEmptySpaceClick(mInitialTouchX); 411 onTrackingStopped(expands); 412 } 413 414 if (mVelocityTracker != null) { 415 mVelocityTracker.recycle(); 416 mVelocityTracker = null; 417 } 418 mPeekTouching = false; 419 } 420 421 private int getFalsingThreshold() { 422 float factor = mStatusBar.isWakeUpComingFromTouch() ? 1.5f : 1.0f; 423 return (int) (mUnlockFalsingThreshold * factor); 424 } 425 426 protected abstract boolean hasConflictingGestures(); 427 428 protected abstract boolean shouldGestureIgnoreXTouchSlop(float x, float y); 429 430 protected void onTrackingStopped(boolean expand) { 431 mTracking = false; 432 mBar.onTrackingStopped(expand); 433 notifyBarPanelExpansionChanged(); 434 } 435 436 protected void onTrackingStarted() { 437 endClosing(); 438 mTracking = true; 439 mCollapseAfterPeek = false; 440 mBar.onTrackingStarted(); 441 notifyExpandingStarted(); 442 notifyBarPanelExpansionChanged(); 443 } 444 445 @Override 446 public boolean onInterceptTouchEvent(MotionEvent event) { 447 if (mInstantExpanding 448 || (mMotionAborted && event.getActionMasked() != MotionEvent.ACTION_DOWN)) { 449 return false; 450 } 451 452 /* 453 * If the user drags anywhere inside the panel we intercept it if the movement is 454 * upwards. This allows closing the shade from anywhere inside the panel. 455 * 456 * We only do this if the current content is scrolled to the bottom, 457 * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture 458 * possible. 459 */ 460 int pointerIndex = event.findPointerIndex(mTrackingPointer); 461 if (pointerIndex < 0) { 462 pointerIndex = 0; 463 mTrackingPointer = event.getPointerId(pointerIndex); 464 } 465 final float x = event.getX(pointerIndex); 466 final float y = event.getY(pointerIndex); 467 boolean scrolledToBottom = isScrolledToBottom(); 468 469 switch (event.getActionMasked()) { 470 case MotionEvent.ACTION_DOWN: 471 mStatusBar.userActivity(); 472 mAnimatingOnDown = mHeightAnimator != null; 473 if (mAnimatingOnDown && mClosing && !mHintAnimationRunning || mPeekPending || mPeekAnimator != null) { 474 cancelHeightAnimator(); 475 cancelPeek(); 476 mTouchSlopExceeded = true; 477 return true; 478 } 479 mInitialTouchY = y; 480 mInitialTouchX = x; 481 mTouchStartedInEmptyArea = !isInContentBounds(x, y); 482 mTouchSlopExceeded = false; 483 mJustPeeked = false; 484 mMotionAborted = false; 485 mPanelClosedOnDown = isFullyCollapsed(); 486 mCollapsedAndHeadsUpOnDown = false; 487 mHasLayoutedSinceDown = false; 488 mUpdateFlingOnLayout = false; 489 mTouchAboveFalsingThreshold = false; 490 initVelocityTracker(); 491 trackMovement(event); 492 break; 493 case MotionEvent.ACTION_POINTER_UP: 494 final int upPointer = event.getPointerId(event.getActionIndex()); 495 if (mTrackingPointer == upPointer) { 496 // gesture is ongoing, find a new pointer to track 497 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 498 mTrackingPointer = event.getPointerId(newIndex); 499 mInitialTouchX = event.getX(newIndex); 500 mInitialTouchY = event.getY(newIndex); 501 } 502 break; 503 case MotionEvent.ACTION_POINTER_DOWN: 504 if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 505 mMotionAborted = true; 506 if (mVelocityTracker != null) { 507 mVelocityTracker.recycle(); 508 mVelocityTracker = null; 509 } 510 } 511 break; 512 case MotionEvent.ACTION_MOVE: 513 final float h = y - mInitialTouchY; 514 trackMovement(event); 515 if (scrolledToBottom || mTouchStartedInEmptyArea || mAnimatingOnDown) { 516 float hAbs = Math.abs(h); 517 if ((h < -mTouchSlop || (mAnimatingOnDown && hAbs > mTouchSlop)) 518 && hAbs > Math.abs(x - mInitialTouchX)) { 519 cancelHeightAnimator(); 520 startExpandMotion(x, y, true /* startTracking */, mExpandedHeight); 521 return true; 522 } 523 } 524 break; 525 case MotionEvent.ACTION_CANCEL: 526 case MotionEvent.ACTION_UP: 527 if (mVelocityTracker != null) { 528 mVelocityTracker.recycle(); 529 mVelocityTracker = null; 530 } 531 break; 532 } 533 return false; 534 } 535 536 /** 537 * @return Whether a pair of coordinates are inside the visible view content bounds. 538 */ 539 protected abstract boolean isInContentBounds(float x, float y); 540 541 protected void cancelHeightAnimator() { 542 if (mHeightAnimator != null) { 543 mHeightAnimator.cancel(); 544 } 545 endClosing(); 546 } 547 548 private void endClosing() { 549 if (mClosing) { 550 mClosing = false; 551 onClosingFinished(); 552 } 553 } 554 555 private void initVelocityTracker() { 556 if (mVelocityTracker != null) { 557 mVelocityTracker.recycle(); 558 } 559 mVelocityTracker = VelocityTrackerFactory.obtain(getContext()); 560 } 561 562 protected boolean isScrolledToBottom() { 563 return true; 564 } 565 566 protected float getContentHeight() { 567 return mExpandedHeight; 568 } 569 570 @Override 571 protected void onFinishInflate() { 572 super.onFinishInflate(); 573 loadDimens(); 574 } 575 576 @Override 577 protected void onConfigurationChanged(Configuration newConfig) { 578 super.onConfigurationChanged(newConfig); 579 loadDimens(); 580 } 581 582 /** 583 * @param vel the current vertical velocity of the motion 584 * @param vectorVel the length of the vectorial velocity 585 * @return whether a fling should expands the panel; contracts otherwise 586 */ 587 protected boolean flingExpands(float vel, float vectorVel, float x, float y) { 588 if (isFalseTouch(x, y)) { 589 return true; 590 } 591 if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 592 return getExpandedFraction() > 0.5f; 593 } else { 594 return vel > 0; 595 } 596 } 597 598 /** 599 * @param x the final x-coordinate when the finger was lifted 600 * @param y the final y-coordinate when the finger was lifted 601 * @return whether this motion should be regarded as a false touch 602 */ 603 private boolean isFalseTouch(float x, float y) { 604 if (!mStatusBar.isFalsingThresholdNeeded()) { 605 return false; 606 } 607 if (mFalsingManager.isClassiferEnabled()) { 608 return mFalsingManager.isFalseTouch(); 609 } 610 if (!mTouchAboveFalsingThreshold) { 611 return true; 612 } 613 if (mUpwardsWhenTresholdReached) { 614 return false; 615 } 616 return !isDirectionUpwards(x, y); 617 } 618 619 protected void fling(float vel, boolean expand) { 620 fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, false); 621 } 622 623 protected void fling(float vel, boolean expand, boolean expandBecauseOfFalsing) { 624 fling(vel, expand, 1.0f /* collapseSpeedUpFactor */, expandBecauseOfFalsing); 625 } 626 627 protected void fling(float vel, boolean expand, float collapseSpeedUpFactor, 628 boolean expandBecauseOfFalsing) { 629 cancelPeek(); 630 float target = expand ? getMaxPanelHeight() : 0.0f; 631 if (!expand) { 632 mClosing = true; 633 } 634 flingToHeight(vel, expand, target, collapseSpeedUpFactor, expandBecauseOfFalsing); 635 } 636 637 protected void flingToHeight(float vel, boolean expand, float target, 638 float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) { 639 // Hack to make the expand transition look nice when clear all button is visible - we make 640 // the animation only to the last notification, and then jump to the maximum panel height so 641 // clear all just fades in and the decelerating motion is towards the last notification. 642 final boolean clearAllExpandHack = expand && fullyExpandedClearAllVisible() 643 && mExpandedHeight < getMaxPanelHeight() - getClearAllHeight() 644 && !isClearAllVisible(); 645 if (clearAllExpandHack) { 646 target = getMaxPanelHeight() - getClearAllHeight(); 647 } 648 if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) { 649 notifyExpandingFinished(); 650 return; 651 } 652 mOverExpandedBeforeFling = getOverExpansionAmount() > 0f; 653 ValueAnimator animator = createHeightAnimator(target); 654 if (expand) { 655 if (expandBecauseOfFalsing) { 656 vel = 0; 657 } 658 mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight()); 659 if (expandBecauseOfFalsing) { 660 animator.setDuration(350); 661 } 662 } else { 663 mFlingAnimationUtils.applyDismissing(animator, mExpandedHeight, target, vel, 664 getHeight()); 665 666 // Make it shorter if we run a canned animation 667 if (vel == 0) { 668 animator.setDuration((long) 669 (animator.getDuration() * getCannedFlingDurationFactor() 670 / collapseSpeedUpFactor)); 671 } 672 } 673 animator.addListener(new AnimatorListenerAdapter() { 674 private boolean mCancelled; 675 676 @Override 677 public void onAnimationCancel(Animator animation) { 678 mCancelled = true; 679 } 680 681 @Override 682 public void onAnimationEnd(Animator animation) { 683 if (clearAllExpandHack && !mCancelled) { 684 setExpandedHeightInternal(getMaxPanelHeight()); 685 } 686 mHeightAnimator = null; 687 if (!mCancelled) { 688 notifyExpandingFinished(); 689 } 690 notifyBarPanelExpansionChanged(); 691 } 692 }); 693 mHeightAnimator = animator; 694 animator.start(); 695 } 696 697 @Override 698 protected void onAttachedToWindow() { 699 super.onAttachedToWindow(); 700 mViewName = getResources().getResourceName(getId()); 701 } 702 703 public String getName() { 704 return mViewName; 705 } 706 707 public void setExpandedHeight(float height) { 708 if (DEBUG) logf("setExpandedHeight(%.1f)", height); 709 setExpandedHeightInternal(height + getOverExpansionPixels()); 710 } 711 712 @Override 713 protected void onLayout (boolean changed, int left, int top, int right, int bottom) { 714 super.onLayout(changed, left, top, right, bottom); 715 mStatusBar.onPanelLaidOut(); 716 requestPanelHeightUpdate(); 717 mHasLayoutedSinceDown = true; 718 if (mUpdateFlingOnLayout) { 719 abortAnimations(); 720 fling(mUpdateFlingVelocity, true /* expands */); 721 mUpdateFlingOnLayout = false; 722 } 723 } 724 725 protected void requestPanelHeightUpdate() { 726 float currentMaxPanelHeight = getMaxPanelHeight(); 727 728 // If the user isn't actively poking us, let's update the height 729 if ((!mTracking || isTrackingBlocked()) 730 && mHeightAnimator == null 731 && !isFullyCollapsed() 732 && currentMaxPanelHeight != mExpandedHeight 733 && !mPeekPending 734 && mPeekAnimator == null 735 && !mPeekTouching) { 736 setExpandedHeight(currentMaxPanelHeight); 737 } 738 } 739 740 public void setExpandedHeightInternal(float h) { 741 float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount(); 742 if (mHeightAnimator == null) { 743 float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion); 744 if (getOverExpansionPixels() != overExpansionPixels && mTracking) { 745 setOverExpansion(overExpansionPixels, true /* isPixels */); 746 } 747 mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount(); 748 } else { 749 mExpandedHeight = h; 750 if (mOverExpandedBeforeFling) { 751 setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */); 752 } 753 } 754 755 mExpandedHeight = Math.max(0, mExpandedHeight); 756 mExpandedFraction = Math.min(1f, fhWithoutOverExpansion == 0 757 ? 0 758 : mExpandedHeight / fhWithoutOverExpansion); 759 onHeightUpdated(mExpandedHeight); 760 notifyBarPanelExpansionChanged(); 761 } 762 763 /** 764 * @return true if the panel tracking should be temporarily blocked; this is used when a 765 * conflicting gesture (opening QS) is happening 766 */ 767 protected abstract boolean isTrackingBlocked(); 768 769 protected abstract void setOverExpansion(float overExpansion, boolean isPixels); 770 771 protected abstract void onHeightUpdated(float expandedHeight); 772 773 protected abstract float getOverExpansionAmount(); 774 775 protected abstract float getOverExpansionPixels(); 776 777 /** 778 * This returns the maximum height of the panel. Children should override this if their 779 * desired height is not the full height. 780 * 781 * @return the default implementation simply returns the maximum height. 782 */ 783 protected abstract int getMaxPanelHeight(); 784 785 public void setExpandedFraction(float frac) { 786 setExpandedHeight(getMaxPanelHeight() * frac); 787 } 788 789 public float getExpandedHeight() { 790 return mExpandedHeight; 791 } 792 793 public float getExpandedFraction() { 794 return mExpandedFraction; 795 } 796 797 public boolean isFullyExpanded() { 798 return mExpandedHeight >= getMaxPanelHeight(); 799 } 800 801 public boolean isFullyCollapsed() { 802 return mExpandedHeight <= 0; 803 } 804 805 public boolean isCollapsing() { 806 return mClosing; 807 } 808 809 public boolean isTracking() { 810 return mTracking; 811 } 812 813 public void setBar(PanelBar panelBar) { 814 mBar = panelBar; 815 } 816 817 public void collapse(boolean delayed, float speedUpFactor) { 818 if (DEBUG) logf("collapse: " + this); 819 if (mPeekPending || mPeekAnimator != null) { 820 mCollapseAfterPeek = true; 821 if (mPeekPending) { 822 823 // We know that the whole gesture is just a peek triggered by a simple click, so 824 // better start it now. 825 removeCallbacks(mPeekRunnable); 826 mPeekRunnable.run(); 827 } 828 } else if (!isFullyCollapsed() && !mTracking && !mClosing) { 829 cancelHeightAnimator(); 830 notifyExpandingStarted(); 831 832 // Set after notifyExpandingStarted, as notifyExpandingStarted resets the closing state. 833 mClosing = true; 834 if (delayed) { 835 mNextCollapseSpeedUpFactor = speedUpFactor; 836 postDelayed(mFlingCollapseRunnable, 120); 837 } else { 838 fling(0, false /* expand */, speedUpFactor, false /* expandBecauseOfFalsing */); 839 } 840 } 841 } 842 843 private final Runnable mFlingCollapseRunnable = new Runnable() { 844 @Override 845 public void run() { 846 fling(0, false /* expand */, mNextCollapseSpeedUpFactor, 847 false /* expandBecauseOfFalsing */); 848 } 849 }; 850 851 public void cancelPeek() { 852 boolean cancelled = mPeekPending; 853 if (mPeekAnimator != null) { 854 cancelled = true; 855 mPeekAnimator.cancel(); 856 } 857 removeCallbacks(mPeekRunnable); 858 mPeekPending = false; 859 860 if (cancelled) { 861 // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also 862 // notify mBar that we might have closed ourselves. 863 notifyBarPanelExpansionChanged(); 864 } 865 } 866 867 public void expand(final boolean animate) { 868 if (!isFullyCollapsed() && !isCollapsing()) { 869 return; 870 } 871 872 mInstantExpanding = true; 873 mUpdateFlingOnLayout = false; 874 abortAnimations(); 875 cancelPeek(); 876 if (mTracking) { 877 onTrackingStopped(true /* expands */); // The panel is expanded after this call. 878 } 879 if (mExpanding) { 880 notifyExpandingFinished(); 881 } 882 notifyBarPanelExpansionChanged(); 883 884 // Wait for window manager to pickup the change, so we know the maximum height of the panel 885 // then. 886 getViewTreeObserver().addOnGlobalLayoutListener( 887 new ViewTreeObserver.OnGlobalLayoutListener() { 888 @Override 889 public void onGlobalLayout() { 890 if (!mInstantExpanding) { 891 getViewTreeObserver().removeOnGlobalLayoutListener(this); 892 return; 893 } 894 if (mStatusBar.getStatusBarWindow().getHeight() 895 != mStatusBar.getStatusBarHeight()) { 896 getViewTreeObserver().removeOnGlobalLayoutListener(this); 897 if (animate) { 898 notifyExpandingStarted(); 899 fling(0, true /* expand */); 900 } else { 901 setExpandedFraction(1f); 902 } 903 mInstantExpanding = false; 904 } 905 } 906 }); 907 908 // Make sure a layout really happens. 909 requestLayout(); 910 } 911 912 public void instantCollapse() { 913 abortAnimations(); 914 setExpandedFraction(0f); 915 if (mExpanding) { 916 notifyExpandingFinished(); 917 } 918 if (mInstantExpanding) { 919 mInstantExpanding = false; 920 notifyBarPanelExpansionChanged(); 921 } 922 } 923 924 private void abortAnimations() { 925 cancelPeek(); 926 cancelHeightAnimator(); 927 removeCallbacks(mPostCollapseRunnable); 928 removeCallbacks(mFlingCollapseRunnable); 929 } 930 931 protected void onClosingFinished() { 932 mBar.onClosingFinished(); 933 } 934 935 936 protected void startUnlockHintAnimation() { 937 938 // We don't need to hint the user if an animation is already running or the user is changing 939 // the expansion. 940 if (mHeightAnimator != null || mTracking) { 941 return; 942 } 943 cancelPeek(); 944 notifyExpandingStarted(); 945 startUnlockHintAnimationPhase1(new Runnable() { 946 @Override 947 public void run() { 948 notifyExpandingFinished(); 949 mStatusBar.onHintFinished(); 950 mHintAnimationRunning = false; 951 } 952 }); 953 mStatusBar.onUnlockHintStarted(); 954 mHintAnimationRunning = true; 955 } 956 957 /** 958 * Phase 1: Move everything upwards. 959 */ 960 private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) { 961 float target = Math.max(0, getMaxPanelHeight() - mHintDistance); 962 ValueAnimator animator = createHeightAnimator(target); 963 animator.setDuration(250); 964 animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 965 animator.addListener(new AnimatorListenerAdapter() { 966 private boolean mCancelled; 967 968 @Override 969 public void onAnimationCancel(Animator animation) { 970 mCancelled = true; 971 } 972 973 @Override 974 public void onAnimationEnd(Animator animation) { 975 if (mCancelled) { 976 mHeightAnimator = null; 977 onAnimationFinished.run(); 978 } else { 979 startUnlockHintAnimationPhase2(onAnimationFinished); 980 } 981 } 982 }); 983 animator.start(); 984 mHeightAnimator = animator; 985 mKeyguardBottomArea.getIndicationView().animate() 986 .translationY(-mHintDistance) 987 .setDuration(250) 988 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) 989 .withEndAction(new Runnable() { 990 @Override 991 public void run() { 992 mKeyguardBottomArea.getIndicationView().animate() 993 .translationY(0) 994 .setDuration(450) 995 .setInterpolator(mBounceInterpolator) 996 .start(); 997 } 998 }) 999 .start(); 1000 } 1001 1002 /** 1003 * Phase 2: Bounce down. 1004 */ 1005 private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) { 1006 ValueAnimator animator = createHeightAnimator(getMaxPanelHeight()); 1007 animator.setDuration(450); 1008 animator.setInterpolator(mBounceInterpolator); 1009 animator.addListener(new AnimatorListenerAdapter() { 1010 @Override 1011 public void onAnimationEnd(Animator animation) { 1012 mHeightAnimator = null; 1013 onAnimationFinished.run(); 1014 notifyBarPanelExpansionChanged(); 1015 } 1016 }); 1017 animator.start(); 1018 mHeightAnimator = animator; 1019 } 1020 1021 private ValueAnimator createHeightAnimator(float targetHeight) { 1022 ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight); 1023 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 1024 @Override 1025 public void onAnimationUpdate(ValueAnimator animation) { 1026 setExpandedHeightInternal((Float) animation.getAnimatedValue()); 1027 } 1028 }); 1029 return animator; 1030 } 1031 1032 protected void notifyBarPanelExpansionChanged() { 1033 mBar.panelExpansionChanged(mExpandedFraction, mExpandedFraction > 0f || mPeekPending 1034 || mPeekAnimator != null || mInstantExpanding || isPanelVisibleBecauseOfHeadsUp() 1035 || mTracking || mHeightAnimator != null); 1036 } 1037 1038 protected abstract boolean isPanelVisibleBecauseOfHeadsUp(); 1039 1040 /** 1041 * Gets called when the user performs a click anywhere in the empty area of the panel. 1042 * 1043 * @return whether the panel will be expanded after the action performed by this method 1044 */ 1045 protected boolean onEmptySpaceClick(float x) { 1046 if (mHintAnimationRunning) { 1047 return true; 1048 } 1049 return onMiddleClicked(); 1050 } 1051 1052 protected final Runnable mPostCollapseRunnable = new Runnable() { 1053 @Override 1054 public void run() { 1055 collapse(false /* delayed */, 1.0f /* speedUpFactor */); 1056 } 1057 }; 1058 1059 protected abstract boolean onMiddleClicked(); 1060 1061 protected abstract boolean isDozing(); 1062 1063 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 1064 pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s" 1065 + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s touchDisabled=%s" 1066 + "]", 1067 this.getClass().getSimpleName(), 1068 getExpandedHeight(), 1069 getMaxPanelHeight(), 1070 mClosing?"T":"f", 1071 mTracking?"T":"f", 1072 mJustPeeked?"T":"f", 1073 mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""), 1074 mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":""), 1075 mTouchDisabled?"T":"f" 1076 )); 1077 } 1078 1079 public abstract void resetViews(); 1080 1081 protected abstract float getPeekHeight(); 1082 1083 protected abstract float getCannedFlingDurationFactor(); 1084 1085 /** 1086 * @return whether "Clear all" button will be visible when the panel is fully expanded 1087 */ 1088 protected abstract boolean fullyExpandedClearAllVisible(); 1089 1090 protected abstract boolean isClearAllVisible(); 1091 1092 /** 1093 * @return the height of the clear all button, in pixels 1094 */ 1095 protected abstract int getClearAllHeight(); 1096 1097 public void setHeadsUpManager(HeadsUpManager headsUpManager) { 1098 mHeadsUpManager = headsUpManager; 1099 } 1100 } 1101