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