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 18 package com.android.systemui; 19 20 import android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.ObjectAnimator; 23 import android.content.Context; 24 import android.util.Log; 25 import android.view.Gravity; 26 import android.view.HapticFeedbackConstants; 27 import android.view.MotionEvent; 28 import android.view.ScaleGestureDetector; 29 import android.view.ScaleGestureDetector.OnScaleGestureListener; 30 import android.view.VelocityTracker; 31 import android.view.View; 32 import android.view.ViewConfiguration; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 36 import com.android.systemui.statusbar.notification.row.ExpandableView; 37 import com.android.systemui.statusbar.policy.ScrollAdapter; 38 import com.android.wm.shell.animation.FlingAnimationUtils; 39 40 public class ExpandHelper implements Gefingerpoken { 41 public interface Callback { getChildAtRawPosition(float x, float y)42 ExpandableView getChildAtRawPosition(float x, float y); getChildAtPosition(float x, float y)43 ExpandableView getChildAtPosition(float x, float y); canChildBeExpanded(View v)44 boolean canChildBeExpanded(View v); setUserExpandedChild(View v, boolean userExpanded)45 void setUserExpandedChild(View v, boolean userExpanded); setUserLockedChild(View v, boolean userLocked)46 void setUserLockedChild(View v, boolean userLocked); expansionStateChanged(boolean isExpanding)47 void expansionStateChanged(boolean isExpanding); getMaxExpandHeight(ExpandableView view)48 int getMaxExpandHeight(ExpandableView view); setExpansionCancelled(View view)49 void setExpansionCancelled(View view); 50 } 51 52 private static final String TAG = "ExpandHelper"; 53 protected static final boolean DEBUG = false; 54 protected static final boolean DEBUG_SCALE = false; 55 private static final float EXPAND_DURATION = 0.3f; 56 57 // Set to false to disable focus-based gestures (spread-finger vertical pull). 58 private static final boolean USE_DRAG = true; 59 // Set to false to disable scale-based gestures (both horizontal and vertical). 60 private static final boolean USE_SPAN = true; 61 // Both gestures types may be active at the same time. 62 // At least one gesture type should be active. 63 // A variant of the screwdriver gesture will emerge from either gesture type. 64 65 // amount of overstretch for maximum brightness expressed in U 66 // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U 67 private static final float STRETCH_INTERVAL = 2f; 68 69 @SuppressWarnings("unused") 70 private Context mContext; 71 72 private boolean mExpanding; 73 private static final int NONE = 0; 74 private static final int BLINDS = 1<<0; 75 private static final int PULL = 1<<1; 76 private static final int STRETCH = 1<<2; 77 private int mExpansionStyle = NONE; 78 private boolean mWatchingForPull; 79 private boolean mHasPopped; 80 private View mEventSource; 81 private float mOldHeight; 82 private float mNaturalHeight; 83 private float mInitialTouchFocusY; 84 private float mInitialTouchX; 85 private float mInitialTouchY; 86 private float mInitialTouchSpan; 87 private float mLastFocusY; 88 private float mLastSpanY; 89 private final int mTouchSlop; 90 private final float mSlopMultiplier; 91 private float mLastMotionY; 92 private float mPullGestureMinXSpan; 93 private Callback mCallback; 94 private ScaleGestureDetector mSGD; 95 private ViewScaler mScaler; 96 private ObjectAnimator mScaleAnimation; 97 private boolean mEnabled = true; 98 private ExpandableView mResizedView; 99 private float mCurrentHeight; 100 101 private int mSmallSize; 102 private int mLargeSize; 103 private float mMaximumStretch; 104 private boolean mOnlyMovements; 105 106 private int mGravity; 107 108 private ScrollAdapter mScrollAdapter; 109 private FlingAnimationUtils mFlingAnimationUtils; 110 private VelocityTracker mVelocityTracker; 111 112 private OnScaleGestureListener mScaleGestureListener 113 = new ScaleGestureDetector.SimpleOnScaleGestureListener() { 114 @Override 115 public boolean onScaleBegin(ScaleGestureDetector detector) { 116 if (DEBUG_SCALE) Log.v(TAG, "onscalebegin()"); 117 118 if (!mOnlyMovements) { 119 startExpanding(mResizedView, STRETCH); 120 } 121 return mExpanding; 122 } 123 124 @Override 125 public boolean onScale(ScaleGestureDetector detector) { 126 if (DEBUG_SCALE) Log.v(TAG, "onscale() on " + mResizedView); 127 return true; 128 } 129 130 @Override 131 public void onScaleEnd(ScaleGestureDetector detector) { 132 } 133 }; 134 135 @VisibleForTesting getScaleAnimation()136 ObjectAnimator getScaleAnimation() { 137 return mScaleAnimation; 138 } 139 140 private class ViewScaler { 141 ExpandableView mView; 142 ViewScaler()143 public ViewScaler() {} setView(ExpandableView v)144 public void setView(ExpandableView v) { 145 mView = v; 146 } setHeight(float h)147 public void setHeight(float h) { 148 if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h); 149 mView.setActualHeight((int) h); 150 mCurrentHeight = h; 151 } getHeight()152 public float getHeight() { 153 return mView.getActualHeight(); 154 } getNaturalHeight()155 public int getNaturalHeight() { 156 return mCallback.getMaxExpandHeight(mView); 157 } 158 } 159 160 /** 161 * Handle expansion gestures to expand and contract children of the callback. 162 * 163 * @param context application context 164 * @param callback the container that holds the items to be manipulated 165 * @param small the smallest allowable size for the manipulated items. 166 * @param large the largest allowable size for the manipulated items. 167 */ ExpandHelper(Context context, Callback callback, int small, int large)168 public ExpandHelper(Context context, Callback callback, int small, int large) { 169 mSmallSize = small; 170 mMaximumStretch = mSmallSize * STRETCH_INTERVAL; 171 mLargeSize = large; 172 mContext = context; 173 mCallback = callback; 174 mScaler = new ViewScaler(); 175 mGravity = Gravity.TOP; 176 mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f); 177 mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min); 178 179 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 180 mTouchSlop = configuration.getScaledTouchSlop(); 181 mSlopMultiplier = configuration.getAmbiguousGestureMultiplier(); 182 183 mSGD = new ScaleGestureDetector(context, mScaleGestureListener); 184 mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(), 185 EXPAND_DURATION); 186 } 187 188 @VisibleForTesting updateExpansion()189 void updateExpansion() { 190 if (DEBUG_SCALE) Log.v(TAG, "updateExpansion()"); 191 // are we scaling or dragging? 192 float span = mSGD.getCurrentSpan() - mInitialTouchSpan; 193 span *= USE_SPAN ? 1f : 0f; 194 float drag = mSGD.getFocusY() - mInitialTouchFocusY; 195 drag *= USE_DRAG ? 1f : 0f; 196 drag *= mGravity == Gravity.BOTTOM ? -1f : 1f; 197 float pull = Math.abs(drag) + Math.abs(span) + 1f; 198 float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull; 199 float target = hand + mOldHeight; 200 float newHeight = clamp(target); 201 mScaler.setHeight(newHeight); 202 mLastFocusY = mSGD.getFocusY(); 203 mLastSpanY = mSGD.getCurrentSpan(); 204 } 205 clamp(float target)206 private float clamp(float target) { 207 float out = target; 208 out = out < mSmallSize ? mSmallSize : out; 209 out = out > mNaturalHeight ? mNaturalHeight : out; 210 return out; 211 } 212 findView(float x, float y)213 private ExpandableView findView(float x, float y) { 214 ExpandableView v; 215 if (mEventSource != null) { 216 int[] location = new int[2]; 217 mEventSource.getLocationOnScreen(location); 218 x += location[0]; 219 y += location[1]; 220 v = mCallback.getChildAtRawPosition(x, y); 221 } else { 222 v = mCallback.getChildAtPosition(x, y); 223 } 224 return v; 225 } 226 isInside(View v, float x, float y)227 private boolean isInside(View v, float x, float y) { 228 if (DEBUG) Log.d(TAG, "isinside (" + x + ", " + y + ")"); 229 230 if (v == null) { 231 if (DEBUG) Log.d(TAG, "isinside null subject"); 232 return false; 233 } 234 if (mEventSource != null) { 235 int[] location = new int[2]; 236 mEventSource.getLocationOnScreen(location); 237 x += location[0]; 238 y += location[1]; 239 if (DEBUG) Log.d(TAG, " to global (" + x + ", " + y + ")"); 240 } 241 int[] location = new int[2]; 242 v.getLocationOnScreen(location); 243 x -= location[0]; 244 y -= location[1]; 245 if (DEBUG) Log.d(TAG, " to local (" + x + ", " + y + ")"); 246 if (DEBUG) Log.d(TAG, " inside (" + v.getWidth() + ", " + v.getHeight() + ")"); 247 boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight()); 248 return inside; 249 } 250 setEventSource(View eventSource)251 public void setEventSource(View eventSource) { 252 mEventSource = eventSource; 253 } 254 setGravity(int gravity)255 public void setGravity(int gravity) { 256 mGravity = gravity; 257 } 258 setScrollAdapter(ScrollAdapter adapter)259 public void setScrollAdapter(ScrollAdapter adapter) { 260 mScrollAdapter = adapter; 261 } 262 getTouchSlop(MotionEvent event)263 private float getTouchSlop(MotionEvent event) { 264 // Adjust the touch slop if another gesture may be being performed. 265 return event.getClassification() == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE 266 ? mTouchSlop * mSlopMultiplier 267 : mTouchSlop; 268 } 269 270 @Override onInterceptTouchEvent(MotionEvent ev)271 public boolean onInterceptTouchEvent(MotionEvent ev) { 272 if (!isEnabled()) { 273 return false; 274 } 275 trackVelocity(ev); 276 final int action = ev.getAction(); 277 if (DEBUG_SCALE) Log.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) + 278 " expanding=" + mExpanding + 279 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 280 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 281 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 282 // check for a spread-finger vertical pull gesture 283 mSGD.onTouchEvent(ev); 284 final int x = (int) mSGD.getFocusX(); 285 final int y = (int) mSGD.getFocusY(); 286 287 mInitialTouchFocusY = y; 288 mInitialTouchSpan = mSGD.getCurrentSpan(); 289 mLastFocusY = mInitialTouchFocusY; 290 mLastSpanY = mInitialTouchSpan; 291 if (DEBUG_SCALE) Log.d(TAG, "set initial span: " + mInitialTouchSpan); 292 293 if (mExpanding) { 294 mLastMotionY = ev.getRawY(); 295 maybeRecycleVelocityTracker(ev); 296 return true; 297 } else { 298 if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) { 299 // we've begun Venetian blinds style expansion 300 return true; 301 } 302 switch (action & MotionEvent.ACTION_MASK) { 303 case MotionEvent.ACTION_MOVE: { 304 final float xspan = mSGD.getCurrentSpanX(); 305 if (xspan > mPullGestureMinXSpan && 306 xspan > mSGD.getCurrentSpanY() && !mExpanding) { 307 // detect a vertical pulling gesture with fingers somewhat separated 308 if (DEBUG_SCALE) Log.v(TAG, "got pull gesture (xspan=" + xspan + "px)"); 309 startExpanding(mResizedView, PULL); 310 mWatchingForPull = false; 311 } 312 if (mWatchingForPull) { 313 final float yDiff = ev.getRawY() - mInitialTouchY; 314 final float xDiff = ev.getRawX() - mInitialTouchX; 315 if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) { 316 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); 317 mWatchingForPull = false; 318 if (mResizedView != null && !isFullyExpanded(mResizedView)) { 319 if (startExpanding(mResizedView, BLINDS)) { 320 mLastMotionY = ev.getRawY(); 321 mInitialTouchY = ev.getRawY(); 322 mHasPopped = false; 323 } 324 } 325 } 326 } 327 break; 328 } 329 330 case MotionEvent.ACTION_DOWN: 331 mWatchingForPull = mScrollAdapter != null && 332 isInside(mScrollAdapter.getHostView(), x, y) 333 && mScrollAdapter.isScrolledToTop(); 334 mResizedView = findView(x, y); 335 if (mResizedView != null && !mCallback.canChildBeExpanded(mResizedView)) { 336 mResizedView = null; 337 mWatchingForPull = false; 338 } 339 mInitialTouchY = ev.getRawY(); 340 mInitialTouchX = ev.getRawX(); 341 break; 342 343 case MotionEvent.ACTION_CANCEL: 344 case MotionEvent.ACTION_UP: 345 if (DEBUG) Log.d(TAG, "up/cancel"); 346 finishExpanding(ev.getActionMasked() == MotionEvent.ACTION_CANCEL /* forceAbort */, 347 getCurrentVelocity()); 348 clearView(); 349 break; 350 } 351 mLastMotionY = ev.getRawY(); 352 maybeRecycleVelocityTracker(ev); 353 return mExpanding; 354 } 355 } 356 trackVelocity(MotionEvent event)357 private void trackVelocity(MotionEvent event) { 358 int action = event.getActionMasked(); 359 switch(action) { 360 case MotionEvent.ACTION_DOWN: 361 if (mVelocityTracker == null) { 362 mVelocityTracker = VelocityTracker.obtain(); 363 } else { 364 mVelocityTracker.clear(); 365 } 366 mVelocityTracker.addMovement(event); 367 break; 368 case MotionEvent.ACTION_MOVE: 369 if (mVelocityTracker == null) { 370 mVelocityTracker = VelocityTracker.obtain(); 371 } 372 mVelocityTracker.addMovement(event); 373 break; 374 default: 375 break; 376 } 377 } 378 maybeRecycleVelocityTracker(MotionEvent event)379 private void maybeRecycleVelocityTracker(MotionEvent event) { 380 if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL 381 || event.getActionMasked() == MotionEvent.ACTION_UP)) { 382 mVelocityTracker.recycle(); 383 mVelocityTracker = null; 384 } 385 } 386 getCurrentVelocity()387 private float getCurrentVelocity() { 388 if (mVelocityTracker != null) { 389 mVelocityTracker.computeCurrentVelocity(1000); 390 return mVelocityTracker.getYVelocity(); 391 } else { 392 return 0f; 393 } 394 } 395 setEnabled(boolean enable)396 public void setEnabled(boolean enable) { 397 mEnabled = enable; 398 } 399 isEnabled()400 private boolean isEnabled() { 401 return mEnabled; 402 } 403 isFullyExpanded(ExpandableView underFocus)404 private boolean isFullyExpanded(ExpandableView underFocus) { 405 return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight() 406 && (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded()); 407 } 408 409 @Override onTouchEvent(MotionEvent ev)410 public boolean onTouchEvent(MotionEvent ev) { 411 if (!isEnabled() && !mExpanding) { 412 // In case we're expanding we still want to finish the current motion. 413 return false; 414 } 415 trackVelocity(ev); 416 final int action = ev.getActionMasked(); 417 if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) + 418 " expanding=" + mExpanding + 419 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") + 420 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") + 421 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : "")); 422 423 mSGD.onTouchEvent(ev); 424 final int x = (int) mSGD.getFocusX(); 425 final int y = (int) mSGD.getFocusY(); 426 427 if (mOnlyMovements) { 428 mLastMotionY = ev.getRawY(); 429 return false; 430 } 431 switch (action) { 432 case MotionEvent.ACTION_DOWN: 433 mWatchingForPull = mScrollAdapter != null && 434 isInside(mScrollAdapter.getHostView(), x, y); 435 mResizedView = findView(x, y); 436 mInitialTouchX = ev.getRawX(); 437 mInitialTouchY = ev.getRawY(); 438 break; 439 case MotionEvent.ACTION_MOVE: { 440 if (mWatchingForPull) { 441 final float yDiff = ev.getRawY() - mInitialTouchY; 442 final float xDiff = ev.getRawX() - mInitialTouchX; 443 if (yDiff > getTouchSlop(ev) && yDiff > Math.abs(xDiff)) { 444 if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)"); 445 mWatchingForPull = false; 446 if (mResizedView != null && !isFullyExpanded(mResizedView)) { 447 if (startExpanding(mResizedView, BLINDS)) { 448 mInitialTouchY = ev.getRawY(); 449 mLastMotionY = ev.getRawY(); 450 mHasPopped = false; 451 } 452 } 453 } 454 } 455 if (mExpanding && 0 != (mExpansionStyle & BLINDS)) { 456 final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight; 457 final float newHeight = clamp(rawHeight); 458 boolean isFinished = false; 459 boolean expanded = false; 460 if (rawHeight > mNaturalHeight) { 461 isFinished = true; 462 expanded = true; 463 } 464 if (rawHeight < mSmallSize) { 465 isFinished = true; 466 expanded = false; 467 } 468 469 if (!mHasPopped) { 470 if (mEventSource != null) { 471 mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 472 } 473 mHasPopped = true; 474 } 475 476 mScaler.setHeight(newHeight); 477 mLastMotionY = ev.getRawY(); 478 if (isFinished) { 479 mCallback.expansionStateChanged(false); 480 } else { 481 mCallback.expansionStateChanged(true); 482 } 483 return true; 484 } 485 486 if (mExpanding) { 487 488 // Gestural expansion is running 489 updateExpansion(); 490 mLastMotionY = ev.getRawY(); 491 return true; 492 } 493 494 break; 495 } 496 497 case MotionEvent.ACTION_POINTER_UP: 498 case MotionEvent.ACTION_POINTER_DOWN: 499 if (DEBUG) Log.d(TAG, "pointer change"); 500 mInitialTouchY += mSGD.getFocusY() - mLastFocusY; 501 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY; 502 break; 503 504 case MotionEvent.ACTION_UP: 505 case MotionEvent.ACTION_CANCEL: 506 if (DEBUG) Log.d(TAG, "up/cancel"); 507 finishExpanding(!isEnabled() || ev.getActionMasked() == MotionEvent.ACTION_CANCEL, 508 getCurrentVelocity()); 509 clearView(); 510 break; 511 } 512 mLastMotionY = ev.getRawY(); 513 maybeRecycleVelocityTracker(ev); 514 return mResizedView != null; 515 } 516 517 /** 518 * @return True if the view is expandable, false otherwise. 519 */ 520 @VisibleForTesting startExpanding(ExpandableView v, int expandType)521 boolean startExpanding(ExpandableView v, int expandType) { 522 if (!(v instanceof ExpandableNotificationRow)) { 523 return false; 524 } 525 mExpansionStyle = expandType; 526 if (mExpanding && v == mResizedView) { 527 return true; 528 } 529 mExpanding = true; 530 mCallback.expansionStateChanged(true); 531 if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v); 532 mCallback.setUserLockedChild(v, true); 533 mScaler.setView(v); 534 mOldHeight = mScaler.getHeight(); 535 mCurrentHeight = mOldHeight; 536 boolean canBeExpanded = mCallback.canChildBeExpanded(v); 537 if (canBeExpanded) { 538 if (DEBUG) Log.d(TAG, "working on an expandable child"); 539 mNaturalHeight = mScaler.getNaturalHeight(); 540 mSmallSize = v.getCollapsedHeight(); 541 } else { 542 if (DEBUG) Log.d(TAG, "working on a non-expandable child"); 543 mNaturalHeight = mOldHeight; 544 } 545 if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight + 546 " mNaturalHeight: " + mNaturalHeight); 547 return true; 548 } 549 550 /** 551 * Finish the current expand motion 552 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old 553 * state 554 * @param velocity the velocity this was expanded/ collapsed with 555 */ 556 @VisibleForTesting finishExpanding(boolean forceAbort, float velocity)557 void finishExpanding(boolean forceAbort, float velocity) { 558 finishExpanding(forceAbort, velocity, true /* allowAnimation */); 559 } 560 561 /** 562 * Finish the current expand motion 563 * @param forceAbort whether the expansion should be forcefully aborted and returned to the old 564 * state 565 * @param velocity the velocity this was expanded/ collapsed with 566 */ finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation)567 private void finishExpanding(boolean forceAbort, float velocity, boolean allowAnimation) { 568 if (!mExpanding) return; 569 570 if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView); 571 572 float currentHeight = mScaler.getHeight(); 573 final boolean wasClosed = (mOldHeight == mSmallSize); 574 boolean nowExpanded; 575 if (!forceAbort) { 576 if (wasClosed) { 577 nowExpanded = currentHeight > mOldHeight && velocity >= 0; 578 } else { 579 nowExpanded = currentHeight >= mOldHeight || velocity > 0; 580 } 581 nowExpanded |= mNaturalHeight == mSmallSize; 582 } else { 583 nowExpanded = !wasClosed; 584 } 585 if (mScaleAnimation.isRunning()) { 586 mScaleAnimation.cancel(); 587 } 588 mCallback.expansionStateChanged(false); 589 int naturalHeight = mScaler.getNaturalHeight(); 590 float targetHeight = nowExpanded ? naturalHeight : mSmallSize; 591 if (targetHeight != currentHeight && mEnabled && allowAnimation) { 592 mScaleAnimation.setFloatValues(targetHeight); 593 mScaleAnimation.setupStartValues(); 594 final View scaledView = mResizedView; 595 final boolean expand = nowExpanded; 596 mScaleAnimation.addListener(new AnimatorListenerAdapter() { 597 public boolean mCancelled; 598 599 @Override 600 public void onAnimationEnd(Animator animation) { 601 if (!mCancelled) { 602 mCallback.setUserExpandedChild(scaledView, expand); 603 if (!mExpanding) { 604 mScaler.setView(null); 605 } 606 } else { 607 mCallback.setExpansionCancelled(scaledView); 608 } 609 mCallback.setUserLockedChild(scaledView, false); 610 mScaleAnimation.removeListener(this); 611 } 612 613 @Override 614 public void onAnimationCancel(Animator animation) { 615 mCancelled = true; 616 } 617 }); 618 velocity = nowExpanded == velocity >= 0 ? velocity : 0; 619 mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity); 620 mScaleAnimation.start(); 621 } else { 622 if (targetHeight != currentHeight) { 623 mScaler.setHeight(targetHeight); 624 } 625 mCallback.setUserExpandedChild(mResizedView, nowExpanded); 626 mCallback.setUserLockedChild(mResizedView, false); 627 mScaler.setView(null); 628 } 629 630 mExpanding = false; 631 mExpansionStyle = NONE; 632 633 if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed); 634 if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight); 635 if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize); 636 if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight); 637 if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView); 638 } 639 clearView()640 private void clearView() { 641 mResizedView = null; 642 } 643 644 /** 645 * Use this to abort any pending expansions in progress and force that there will be no 646 * animations. 647 */ cancelImmediately()648 public void cancelImmediately() { 649 cancel(false /* allowAnimation */); 650 } 651 652 /** 653 * Use this to abort any pending expansions in progress. 654 */ cancel()655 public void cancel() { 656 cancel(true /* allowAnimation */); 657 } 658 cancel(boolean allowAnimation)659 private void cancel(boolean allowAnimation) { 660 finishExpanding(true /* forceAbort */, 0f /* velocity */, allowAnimation); 661 clearView(); 662 663 // reset the gesture detector 664 mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener); 665 } 666 667 /** 668 * Change the expansion mode to only observe movements and don't perform any resizing. 669 * This is needed when the expanding is finished and the scroller kicks in, 670 * performing an overscroll motion. We only want to shrink it again when we are not 671 * overscrolled. 672 * 673 * @param onlyMovements Should only movements be observed? 674 */ onlyObserveMovements(boolean onlyMovements)675 public void onlyObserveMovements(boolean onlyMovements) { 676 mOnlyMovements = onlyMovements; 677 } 678 } 679 680