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