PanelView.java revision 93439da0ed9213bc54291b3fce4e04dd3a7f9f3a
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.content.res.Resources; 26import android.util.AttributeSet; 27import android.util.Log; 28import android.view.MotionEvent; 29import android.view.ViewConfiguration; 30import android.view.ViewTreeObserver; 31import android.view.animation.AnimationUtils; 32import android.view.animation.Interpolator; 33import android.widget.FrameLayout; 34 35import com.android.systemui.R; 36import com.android.systemui.statusbar.FlingAnimationUtils; 37import com.android.systemui.statusbar.StatusBarState; 38 39import java.io.FileDescriptor; 40import java.io.PrintWriter; 41 42public abstract class PanelView extends FrameLayout { 43 public static final boolean DEBUG = PanelBar.DEBUG; 44 public static final String TAG = PanelView.class.getSimpleName(); 45 46 private final void logf(String fmt, Object... args) { 47 Log.v(TAG, (mViewName != null ? (mViewName + ": ") : "") + String.format(fmt, args)); 48 } 49 50 protected PhoneStatusBar mStatusBar; 51 private float mPeekHeight; 52 private float mHintDistance; 53 private int mEdgeTapAreaWidth; 54 private float mInitialOffsetOnTouch; 55 private float mExpandedFraction = 0; 56 protected float mExpandedHeight = 0; 57 private boolean mJustPeeked; 58 private boolean mClosing; 59 protected boolean mTracking; 60 private boolean mTouchSlopExceeded; 61 private int mTrackingPointer; 62 protected int mTouchSlop; 63 protected boolean mHintAnimationRunning; 64 private boolean mOverExpandedBeforeFling; 65 private float mOriginalIndicationY; 66 67 private ValueAnimator mHeightAnimator; 68 private ObjectAnimator mPeekAnimator; 69 private VelocityTrackerInterface mVelocityTracker; 70 private FlingAnimationUtils mFlingAnimationUtils; 71 72 /** 73 * Whether an instant expand request is currently pending and we are just waiting for layout. 74 */ 75 private boolean mInstantExpanding; 76 77 PanelBar mBar; 78 79 protected int mMaxPanelHeight = -1; 80 private String mViewName; 81 private float mInitialTouchY; 82 private float mInitialTouchX; 83 84 private Interpolator mLinearOutSlowInInterpolator; 85 private Interpolator mFastOutSlowInInterpolator; 86 private Interpolator mBounceInterpolator; 87 protected KeyguardBottomAreaView mKeyguardBottomArea; 88 89 private boolean mPeekPending; 90 private boolean mCollapseAfterPeek; 91 private boolean mExpanding; 92 private boolean mGestureWaitForTouchSlop; 93 private Runnable mPeekRunnable = new Runnable() { 94 @Override 95 public void run() { 96 mPeekPending = false; 97 runPeekAnimation(); 98 } 99 }; 100 101 protected void onExpandingFinished() { 102 mBar.onExpandingFinished(); 103 } 104 105 protected void onExpandingStarted() { 106 } 107 108 private void notifyExpandingStarted() { 109 if (!mExpanding) { 110 mExpanding = true; 111 onExpandingStarted(); 112 } 113 } 114 115 private void notifyExpandingFinished() { 116 if (mExpanding) { 117 mExpanding = false; 118 onExpandingFinished(); 119 } 120 } 121 122 private void schedulePeek() { 123 mPeekPending = true; 124 long timeout = ViewConfiguration.getTapTimeout(); 125 postOnAnimationDelayed(mPeekRunnable, timeout); 126 notifyBarPanelExpansionChanged(); 127 } 128 129 private void runPeekAnimation() { 130 mPeekHeight = getPeekHeight(); 131 if (DEBUG) logf("peek to height=%.1f", mPeekHeight); 132 if (mHeightAnimator != null) { 133 return; 134 } 135 mPeekAnimator = ObjectAnimator.ofFloat(this, "expandedHeight", mPeekHeight) 136 .setDuration(250); 137 mPeekAnimator.setInterpolator(mLinearOutSlowInInterpolator); 138 mPeekAnimator.addListener(new AnimatorListenerAdapter() { 139 private boolean mCancelled; 140 141 @Override 142 public void onAnimationCancel(Animator animation) { 143 mCancelled = true; 144 } 145 146 @Override 147 public void onAnimationEnd(Animator animation) { 148 mPeekAnimator = null; 149 if (mCollapseAfterPeek && !mCancelled) { 150 postOnAnimation(new Runnable() { 151 @Override 152 public void run() { 153 collapse(); 154 } 155 }); 156 } 157 mCollapseAfterPeek = false; 158 } 159 }); 160 notifyExpandingStarted(); 161 mPeekAnimator.start(); 162 mJustPeeked = true; 163 } 164 165 public PanelView(Context context, AttributeSet attrs) { 166 super(context, attrs); 167 mFlingAnimationUtils = new FlingAnimationUtils(context, 0.6f); 168 mFastOutSlowInInterpolator = 169 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); 170 mLinearOutSlowInInterpolator = 171 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); 172 mBounceInterpolator = new BounceInterpolator(); 173 } 174 175 protected void loadDimens() { 176 final Resources res = getContext().getResources(); 177 final ViewConfiguration configuration = ViewConfiguration.get(getContext()); 178 mTouchSlop = configuration.getScaledTouchSlop(); 179 mHintDistance = res.getDimension(R.dimen.hint_move_distance); 180 mEdgeTapAreaWidth = res.getDimensionPixelSize(R.dimen.edge_tap_area_width); 181 } 182 183 private void trackMovement(MotionEvent event) { 184 // Add movement to velocity tracker using raw screen X and Y coordinates instead 185 // of window coordinates because the window frame may be moving at the same time. 186 float deltaX = event.getRawX() - event.getX(); 187 float deltaY = event.getRawY() - event.getY(); 188 event.offsetLocation(deltaX, deltaY); 189 if (mVelocityTracker != null) mVelocityTracker.addMovement(event); 190 event.offsetLocation(-deltaX, -deltaY); 191 } 192 193 @Override 194 public boolean onTouchEvent(MotionEvent event) { 195 if (mInstantExpanding) { 196 return false; 197 } 198 199 /* 200 * We capture touch events here and update the expand height here in case according to 201 * the users fingers. This also handles multi-touch. 202 * 203 * If the user just clicks shortly, we give him a quick peek of the shade. 204 * 205 * Flinging is also enabled in order to open or close the shade. 206 */ 207 208 int pointerIndex = event.findPointerIndex(mTrackingPointer); 209 if (pointerIndex < 0) { 210 pointerIndex = 0; 211 mTrackingPointer = event.getPointerId(pointerIndex); 212 } 213 final float y = event.getY(pointerIndex); 214 final float x = event.getX(pointerIndex); 215 216 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 217 mGestureWaitForTouchSlop = mExpandedHeight == 0f; 218 } 219 boolean waitForTouchSlop = hasConflictingGestures() || mGestureWaitForTouchSlop; 220 221 switch (event.getActionMasked()) { 222 case MotionEvent.ACTION_DOWN: 223 mInitialTouchY = y; 224 mInitialTouchX = x; 225 mInitialOffsetOnTouch = mExpandedHeight; 226 mTouchSlopExceeded = false; 227 mJustPeeked = false; 228 if (mVelocityTracker == null) { 229 initVelocityTracker(); 230 } 231 trackMovement(event); 232 if (!waitForTouchSlop || (mHeightAnimator != null && !mHintAnimationRunning) || 233 mPeekPending || mPeekAnimator != null) { 234 if (mHeightAnimator != null) { 235 mHeightAnimator.cancel(); // end any outstanding animations 236 } 237 cancelPeek(); 238 mTouchSlopExceeded = (mHeightAnimator != null && !mHintAnimationRunning) 239 || mPeekPending || mPeekAnimator != null; 240 onTrackingStarted(); 241 } 242 if (mExpandedHeight == 0) { 243 schedulePeek(); 244 } 245 break; 246 247 case MotionEvent.ACTION_POINTER_UP: 248 final int upPointer = event.getPointerId(event.getActionIndex()); 249 if (mTrackingPointer == upPointer) { 250 // gesture is ongoing, find a new pointer to track 251 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 252 final float newY = event.getY(newIndex); 253 final float newX = event.getX(newIndex); 254 mTrackingPointer = event.getPointerId(newIndex); 255 mInitialOffsetOnTouch = mExpandedHeight; 256 mInitialTouchY = newY; 257 mInitialTouchX = newX; 258 } 259 break; 260 261 case MotionEvent.ACTION_MOVE: 262 float h = y - mInitialTouchY; 263 264 // If the panel was collapsed when touching, we only need to check for the 265 // y-component of the gesture, as we have no conflicting horizontal gesture. 266 if (Math.abs(h) > mTouchSlop 267 && (Math.abs(h) > Math.abs(x - mInitialTouchX) 268 || mInitialOffsetOnTouch == 0f)) { 269 mTouchSlopExceeded = true; 270 if (waitForTouchSlop && !mTracking) { 271 if (!mJustPeeked) { 272 mInitialOffsetOnTouch = mExpandedHeight; 273 mInitialTouchX = x; 274 mInitialTouchY = y; 275 h = 0; 276 } 277 if (mHeightAnimator != null) { 278 mHeightAnimator.cancel(); // end any outstanding animations 279 } 280 removeCallbacks(mPeekRunnable); 281 mPeekPending = false; 282 onTrackingStarted(); 283 } 284 } 285 final float newHeight = Math.max(0, h + mInitialOffsetOnTouch); 286 if (newHeight > mPeekHeight) { 287 if (mPeekAnimator != null) { 288 mPeekAnimator.cancel(); 289 } 290 mJustPeeked = false; 291 } 292 if (!mJustPeeked && (!waitForTouchSlop || mTracking)) { 293 setExpandedHeightInternal(newHeight); 294 } 295 296 trackMovement(event); 297 break; 298 299 case MotionEvent.ACTION_UP: 300 case MotionEvent.ACTION_CANCEL: 301 mTrackingPointer = -1; 302 trackMovement(event); 303 if ((mTracking && mTouchSlopExceeded) 304 || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { 305 float vel = 0f; 306 float vectorVel = 0f; 307 if (mVelocityTracker != null) { 308 mVelocityTracker.computeCurrentVelocity(1000); 309 vel = mVelocityTracker.getYVelocity(); 310 vectorVel = (float) Math.hypot( 311 mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); 312 } 313 boolean expand = flingExpands(vel, vectorVel); 314 onTrackingStopped(expand); 315 fling(vel, expand); 316 } else { 317 boolean expands = onEmptySpaceClick(mInitialTouchX); 318 onTrackingStopped(expands); 319 } 320 if (mVelocityTracker != null) { 321 mVelocityTracker.recycle(); 322 mVelocityTracker = null; 323 } 324 break; 325 } 326 return !waitForTouchSlop || mTracking; 327 } 328 329 protected abstract boolean hasConflictingGestures(); 330 331 protected void onTrackingStopped(boolean expand) { 332 mTracking = false; 333 mBar.onTrackingStopped(PanelView.this, expand); 334 } 335 336 protected void onTrackingStarted() { 337 mTracking = true; 338 mCollapseAfterPeek = false; 339 mBar.onTrackingStarted(PanelView.this); 340 notifyExpandingStarted(); 341 } 342 343 @Override 344 public boolean onInterceptTouchEvent(MotionEvent event) { 345 if (mInstantExpanding) { 346 return false; 347 } 348 349 /* 350 * If the user drags anywhere inside the panel we intercept it if he moves his finger 351 * upwards. This allows closing the shade from anywhere inside the panel. 352 * 353 * We only do this if the current content is scrolled to the bottom, 354 * i.e isScrolledToBottom() is true and therefore there is no conflicting scrolling gesture 355 * possible. 356 */ 357 int pointerIndex = event.findPointerIndex(mTrackingPointer); 358 if (pointerIndex < 0) { 359 pointerIndex = 0; 360 mTrackingPointer = event.getPointerId(pointerIndex); 361 } 362 final float x = event.getX(pointerIndex); 363 final float y = event.getY(pointerIndex); 364 boolean scrolledToBottom = isScrolledToBottom(); 365 366 switch (event.getActionMasked()) { 367 case MotionEvent.ACTION_DOWN: 368 if (mHeightAnimator != null && !mHintAnimationRunning || 369 mPeekPending || mPeekAnimator != null) { 370 if (mHeightAnimator != null) { 371 mHeightAnimator.cancel(); // end any outstanding animations 372 } 373 cancelPeek(); 374 mTouchSlopExceeded = true; 375 return true; 376 } 377 mInitialTouchY = y; 378 mInitialTouchX = x; 379 mTouchSlopExceeded = false; 380 mJustPeeked = false; 381 initVelocityTracker(); 382 trackMovement(event); 383 break; 384 case MotionEvent.ACTION_POINTER_UP: 385 final int upPointer = event.getPointerId(event.getActionIndex()); 386 if (mTrackingPointer == upPointer) { 387 // gesture is ongoing, find a new pointer to track 388 final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; 389 mTrackingPointer = event.getPointerId(newIndex); 390 mInitialTouchX = event.getX(newIndex); 391 mInitialTouchY = event.getY(newIndex); 392 } 393 break; 394 395 case MotionEvent.ACTION_MOVE: 396 final float h = y - mInitialTouchY; 397 trackMovement(event); 398 if (scrolledToBottom) { 399 if (h < -mTouchSlop && h < -Math.abs(x - mInitialTouchX)) { 400 if (mHeightAnimator != null) { 401 mHeightAnimator.cancel(); 402 } 403 mInitialOffsetOnTouch = mExpandedHeight; 404 mInitialTouchY = y; 405 mInitialTouchX = x; 406 mTracking = true; 407 mTouchSlopExceeded = true; 408 onTrackingStarted(); 409 return true; 410 } 411 } 412 break; 413 } 414 return false; 415 } 416 417 private void initVelocityTracker() { 418 if (mVelocityTracker != null) { 419 mVelocityTracker.recycle(); 420 } 421 mVelocityTracker = VelocityTrackerFactory.obtain(getContext()); 422 } 423 424 protected boolean isScrolledToBottom() { 425 return true; 426 } 427 428 protected float getContentHeight() { 429 return mExpandedHeight; 430 } 431 432 @Override 433 protected void onFinishInflate() { 434 super.onFinishInflate(); 435 loadDimens(); 436 } 437 438 @Override 439 protected void onConfigurationChanged(Configuration newConfig) { 440 super.onConfigurationChanged(newConfig); 441 loadDimens(); 442 mMaxPanelHeight = -1; 443 } 444 445 /** 446 * @param vel the current vertical velocity of the motion 447 * @param vectorVel the length of the vectorial velocity 448 * @return whether a fling should expands the panel; contracts otherwise 449 */ 450 private boolean flingExpands(float vel, float vectorVel) { 451 if (Math.abs(vectorVel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 452 return getExpandedFraction() > 0.5f; 453 } else { 454 return vel > 0; 455 } 456 } 457 458 protected void fling(float vel, boolean expand) { 459 cancelPeek(); 460 float target = expand ? getMaxPanelHeight() : 0.0f; 461 if (target == mExpandedHeight || getOverExpansionAmount() > 0f && expand) { 462 notifyExpandingFinished(); 463 return; 464 } 465 mOverExpandedBeforeFling = getOverExpansionAmount() > 0f; 466 ValueAnimator animator = createHeightAnimator(target); 467 if (expand) { 468 mFlingAnimationUtils.apply(animator, mExpandedHeight, target, vel, getHeight()); 469 } else { 470 mFlingAnimationUtils.applyDismissing(animator, mExpandedHeight, target, vel, 471 getHeight()); 472 473 // Make it shorter if we run a canned animation 474 if (vel == 0) { 475 animator.setDuration((long) (animator.getDuration() / 1.75f)); 476 } 477 } 478 animator.addListener(new AnimatorListenerAdapter() { 479 private boolean mCancelled; 480 481 @Override 482 public void onAnimationCancel(Animator animation) { 483 mCancelled = true; 484 } 485 486 @Override 487 public void onAnimationEnd(Animator animation) { 488 mHeightAnimator = null; 489 if (!mCancelled) { 490 notifyExpandingFinished(); 491 } 492 } 493 }); 494 mHeightAnimator = animator; 495 animator.start(); 496 } 497 498 @Override 499 protected void onAttachedToWindow() { 500 super.onAttachedToWindow(); 501 mViewName = getResources().getResourceName(getId()); 502 } 503 504 public String getName() { 505 return mViewName; 506 } 507 508 @Override 509 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 510 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 511 512 if (DEBUG) logf("onMeasure(%d, %d) -> (%d, %d)", 513 widthMeasureSpec, heightMeasureSpec, getMeasuredWidth(), getMeasuredHeight()); 514 515 // Did one of our children change size? 516 int newHeight = getMeasuredHeight(); 517 if (newHeight > mMaxPanelHeight) { 518 // we only adapt the max height if it's bigger 519 mMaxPanelHeight = newHeight; 520 // If the user isn't actively poking us, let's rubberband to the content 521 if (!mTracking && mHeightAnimator == null 522 && mExpandedHeight > 0 && mExpandedHeight != mMaxPanelHeight 523 && mMaxPanelHeight > 0 && mPeekAnimator == null) { 524 mExpandedHeight = mMaxPanelHeight; 525 } 526 } 527 } 528 529 public void setExpandedHeight(float height) { 530 if (DEBUG) logf("setExpandedHeight(%.1f)", height); 531 setExpandedHeightInternal(height + getOverExpansionPixels()); 532 } 533 534 @Override 535 protected void onLayout (boolean changed, int left, int top, int right, int bottom) { 536 if (DEBUG) logf("onLayout: changed=%s, bottom=%d eh=%d fh=%d", changed?"T":"f", bottom, 537 (int)mExpandedHeight, mMaxPanelHeight); 538 super.onLayout(changed, left, top, right, bottom); 539 requestPanelHeightUpdate(); 540 } 541 542 protected void requestPanelHeightUpdate() { 543 float currentMaxPanelHeight = getMaxPanelHeight(); 544 545 // If the user isn't actively poking us, let's update the height 546 if (!mTracking && mHeightAnimator == null 547 && mExpandedHeight > 0 && currentMaxPanelHeight != mExpandedHeight 548 && !mPeekPending && mPeekAnimator == null) { 549 setExpandedHeight(currentMaxPanelHeight); 550 } 551 } 552 553 public void setExpandedHeightInternal(float h) { 554 float fhWithoutOverExpansion = getMaxPanelHeight() - getOverExpansionAmount(); 555 if (mHeightAnimator == null) { 556 float overExpansionPixels = Math.max(0, h - fhWithoutOverExpansion); 557 if (getOverExpansionPixels() != overExpansionPixels && mTracking) { 558 setOverExpansion(overExpansionPixels, true /* isPixels */); 559 } 560 mExpandedHeight = Math.min(h, fhWithoutOverExpansion) + getOverExpansionAmount(); 561 } else { 562 mExpandedHeight = h; 563 if (mOverExpandedBeforeFling) { 564 setOverExpansion(Math.max(0, h - fhWithoutOverExpansion), false /* isPixels */); 565 } 566 } 567 568 mExpandedHeight = Math.max(0, mExpandedHeight); 569 onHeightUpdated(mExpandedHeight); 570 mExpandedFraction = Math.min(1f, fhWithoutOverExpansion == 0 571 ? 0 572 : mExpandedHeight / fhWithoutOverExpansion); 573 notifyBarPanelExpansionChanged(); 574 } 575 576 protected abstract void setOverExpansion(float overExpansion, boolean isPixels); 577 578 protected abstract void onHeightUpdated(float expandedHeight); 579 580 protected abstract float getOverExpansionAmount(); 581 582 protected abstract float getOverExpansionPixels(); 583 584 /** 585 * This returns the maximum height of the panel. Children should override this if their 586 * desired height is not the full height. 587 * 588 * @return the default implementation simply returns the maximum height. 589 */ 590 protected int getMaxPanelHeight() { 591 mMaxPanelHeight = Math.max(mMaxPanelHeight, getHeight()); 592 return mMaxPanelHeight; 593 } 594 595 public void setExpandedFraction(float frac) { 596 setExpandedHeight(getMaxPanelHeight() * frac); 597 } 598 599 public float getExpandedHeight() { 600 return mExpandedHeight; 601 } 602 603 public float getExpandedFraction() { 604 return mExpandedFraction; 605 } 606 607 public boolean isFullyExpanded() { 608 return mExpandedHeight >= getMaxPanelHeight(); 609 } 610 611 public boolean isFullyCollapsed() { 612 return mExpandedHeight <= 0; 613 } 614 615 public boolean isCollapsing() { 616 return mClosing; 617 } 618 619 public boolean isTracking() { 620 return mTracking; 621 } 622 623 public void setBar(PanelBar panelBar) { 624 mBar = panelBar; 625 } 626 627 public void collapse() { 628 // TODO: abort animation or ongoing touch 629 if (DEBUG) logf("collapse: " + this); 630 if (mPeekPending || mPeekAnimator != null) { 631 mCollapseAfterPeek = true; 632 if (mPeekPending) { 633 634 // We know that the whole gesture is just a peek triggered by a simple click, so 635 // better start it now. 636 removeCallbacks(mPeekRunnable); 637 mPeekRunnable.run(); 638 } 639 } else if (!isFullyCollapsed()) { 640 if (mHeightAnimator != null) { 641 mHeightAnimator.cancel(); 642 } 643 mClosing = true; 644 notifyExpandingStarted(); 645 fling(0, false /* expand */); 646 } 647 } 648 649 public void expand() { 650 if (DEBUG) logf("expand: " + this); 651 if (isFullyCollapsed()) { 652 mBar.startOpeningPanel(this); 653 notifyExpandingStarted(); 654 fling(0, true /* expand */); 655 } else if (DEBUG) { 656 if (DEBUG) logf("skipping expansion: is expanded"); 657 } 658 } 659 660 public void cancelPeek() { 661 if (mPeekAnimator != null) { 662 mPeekAnimator.cancel(); 663 } 664 removeCallbacks(mPeekRunnable); 665 mPeekPending = false; 666 667 // When peeking, we already tell mBar that we expanded ourselves. Make sure that we also 668 // notify mBar that we might have closed ourselves. 669 notifyBarPanelExpansionChanged(); 670 } 671 672 public void instantExpand() { 673 mInstantExpanding = true; 674 abortAnimations(); 675 if (mTracking) { 676 onTrackingStopped(true /* expands */); // The panel is expanded after this call. 677 notifyExpandingFinished(); 678 } 679 setVisibility(VISIBLE); 680 681 // Wait for window manager to pickup the change, so we know the maximum height of the panel 682 // then. 683 getViewTreeObserver().addOnGlobalLayoutListener( 684 new ViewTreeObserver.OnGlobalLayoutListener() { 685 @Override 686 public void onGlobalLayout() { 687 if (mStatusBar.getStatusBarWindow().getHeight() 688 != mStatusBar.getStatusBarHeight()) { 689 getViewTreeObserver().removeOnGlobalLayoutListener(this); 690 setExpandedFraction(1f); 691 mInstantExpanding = false; 692 } 693 } 694 }); 695 696 // Make sure a layout really happens. 697 requestLayout(); 698 } 699 700 private void abortAnimations() { 701 cancelPeek(); 702 if (mHeightAnimator != null) { 703 mHeightAnimator.cancel(); 704 } 705 } 706 707 protected void startUnlockHintAnimation() { 708 709 // We don't need to hint the user if an animation is already running or the user is changing 710 // the expansion. 711 if (mHeightAnimator != null || mTracking) { 712 return; 713 } 714 cancelPeek(); 715 notifyExpandingStarted(); 716 startUnlockHintAnimationPhase1(new Runnable() { 717 @Override 718 public void run() { 719 notifyExpandingFinished(); 720 mStatusBar.onHintFinished(); 721 mHintAnimationRunning = false; 722 } 723 }); 724 mStatusBar.onUnlockHintStarted(); 725 mHintAnimationRunning = true; 726 } 727 728 /** 729 * Phase 1: Move everything upwards. 730 */ 731 private void startUnlockHintAnimationPhase1(final Runnable onAnimationFinished) { 732 float target = Math.max(0, getMaxPanelHeight() - mHintDistance); 733 ValueAnimator animator = createHeightAnimator(target); 734 animator.setDuration(250); 735 animator.setInterpolator(mFastOutSlowInInterpolator); 736 animator.addListener(new AnimatorListenerAdapter() { 737 private boolean mCancelled; 738 739 @Override 740 public void onAnimationCancel(Animator animation) { 741 mCancelled = true; 742 } 743 744 @Override 745 public void onAnimationEnd(Animator animation) { 746 if (mCancelled) { 747 mHeightAnimator = null; 748 onAnimationFinished.run(); 749 } else { 750 startUnlockHintAnimationPhase2(onAnimationFinished); 751 } 752 } 753 }); 754 animator.start(); 755 mHeightAnimator = animator; 756 mOriginalIndicationY = mKeyguardBottomArea.getIndicationView().getY(); 757 mKeyguardBottomArea.getIndicationView().animate() 758 .y(mOriginalIndicationY - mHintDistance) 759 .setDuration(250) 760 .setInterpolator(mFastOutSlowInInterpolator) 761 .withEndAction(new Runnable() { 762 @Override 763 public void run() { 764 mKeyguardBottomArea.getIndicationView().animate() 765 .y(mOriginalIndicationY) 766 .setDuration(450) 767 .setInterpolator(mBounceInterpolator) 768 .start(); 769 } 770 }) 771 .start(); 772 } 773 774 /** 775 * Phase 2: Bounce down. 776 */ 777 private void startUnlockHintAnimationPhase2(final Runnable onAnimationFinished) { 778 ValueAnimator animator = createHeightAnimator(getMaxPanelHeight()); 779 animator.setDuration(450); 780 animator.setInterpolator(mBounceInterpolator); 781 animator.addListener(new AnimatorListenerAdapter() { 782 @Override 783 public void onAnimationEnd(Animator animation) { 784 mHeightAnimator = null; 785 onAnimationFinished.run(); 786 } 787 }); 788 animator.start(); 789 mHeightAnimator = animator; 790 } 791 792 private ValueAnimator createHeightAnimator(float targetHeight) { 793 ValueAnimator animator = ValueAnimator.ofFloat(mExpandedHeight, targetHeight); 794 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 795 @Override 796 public void onAnimationUpdate(ValueAnimator animation) { 797 setExpandedHeightInternal((Float) animation.getAnimatedValue()); 798 } 799 }); 800 return animator; 801 } 802 803 private void notifyBarPanelExpansionChanged() { 804 mBar.panelExpansionChanged(this, mExpandedFraction, mExpandedFraction > 0f || mPeekPending 805 || mPeekAnimator != null); 806 } 807 808 /** 809 * Gets called when the user performs a click anywhere in the empty area of the panel. 810 * 811 * @return whether the panel will be expanded after the action performed by this method 812 */ 813 private boolean onEmptySpaceClick(float x) { 814 if (mHintAnimationRunning) { 815 return true; 816 } 817 if (x < mEdgeTapAreaWidth 818 && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 819 onEdgeClicked(false /* right */); 820 return true; 821 } else if (x > getWidth() - mEdgeTapAreaWidth 822 && mStatusBar.getBarState() == StatusBarState.KEYGUARD) { 823 onEdgeClicked(true /* right */); 824 return true; 825 } else { 826 return onMiddleClicked(); 827 } 828 } 829 830 private boolean onMiddleClicked() { 831 switch (mStatusBar.getBarState()) { 832 case StatusBarState.KEYGUARD: 833 startUnlockHintAnimation(); 834 return true; 835 case StatusBarState.SHADE_LOCKED: 836 mStatusBar.goToKeyguard(); 837 return true; 838 case StatusBarState.SHADE: 839 collapse(); 840 return false; 841 default: 842 return true; 843 } 844 } 845 846 protected abstract void onEdgeClicked(boolean right); 847 848 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 849 pw.println(String.format("[PanelView(%s): expandedHeight=%f maxPanelHeight=%d closing=%s" 850 + " tracking=%s justPeeked=%s peekAnim=%s%s timeAnim=%s%s" 851 + "]", 852 this.getClass().getSimpleName(), 853 getExpandedHeight(), 854 getMaxPanelHeight(), 855 mClosing?"T":"f", 856 mTracking?"T":"f", 857 mJustPeeked?"T":"f", 858 mPeekAnimator, ((mPeekAnimator!=null && mPeekAnimator.isStarted())?" (started)":""), 859 mHeightAnimator, ((mHeightAnimator !=null && mHeightAnimator.isStarted())?" (started)":"") 860 )); 861 } 862 863 public abstract void resetViews(); 864 865 protected abstract float getPeekHeight(); 866} 867