Home | History | Annotate | Download | only in systemui
      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 {
     41         ExpandableView getChildAtRawPosition(float x, float y);
     42         ExpandableView getChildAtPosition(float x, float y);
     43         boolean canChildBeExpanded(View v);
     44         void setUserExpandedChild(View v, boolean userExpanded);
     45         void setUserLockedChild(View v, boolean userLocked);
     46         void expansionStateChanged(boolean isExpanding);
     47         int getMaxExpandHeight(ExpandableView 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 
    136         public ViewScaler() {}
    137         public void setView(ExpandableView v) {
    138             mView = v;
    139         }
    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         }
    145         public float getHeight() {
    146             return mView.getActualHeight();
    147         }
    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      */
    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 
    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 
    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 
    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 
    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 
    241     public void setEventSource(View eventSource) {
    242         mEventSource = eventSource;
    243     }
    244 
    245     public void setGravity(int gravity) {
    246         mGravity = gravity;
    247     }
    248 
    249     public void setScrollAdapter(ScrollAdapter adapter) {
    250         mScrollAdapter = adapter;
    251     }
    252 
    253     @Override
    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(false, getCurrentVelocity());
    330                 clearView();
    331                 break;
    332             }
    333             mLastMotionY = ev.getRawY();
    334             maybeRecycleVelocityTracker(ev);
    335             return mExpanding;
    336         }
    337     }
    338 
    339     private void trackVelocity(MotionEvent event) {
    340         int action = event.getActionMasked();
    341         switch(action) {
    342             case MotionEvent.ACTION_DOWN:
    343                 if (mVelocityTracker == null) {
    344                     mVelocityTracker = VelocityTracker.obtain();
    345                 } else {
    346                     mVelocityTracker.clear();
    347                 }
    348                 mVelocityTracker.addMovement(event);
    349                 break;
    350             case MotionEvent.ACTION_MOVE:
    351                 if (mVelocityTracker == null) {
    352                     mVelocityTracker = VelocityTracker.obtain();
    353                 }
    354                 mVelocityTracker.addMovement(event);
    355                 break;
    356             default:
    357                 break;
    358         }
    359     }
    360 
    361     private void maybeRecycleVelocityTracker(MotionEvent event) {
    362         if (mVelocityTracker != null && (event.getActionMasked() == MotionEvent.ACTION_CANCEL
    363                 || event.getActionMasked() == MotionEvent.ACTION_UP)) {
    364             mVelocityTracker.recycle();
    365             mVelocityTracker = null;
    366         }
    367     }
    368 
    369     private float getCurrentVelocity() {
    370         if (mVelocityTracker != null) {
    371             mVelocityTracker.computeCurrentVelocity(1000);
    372             return mVelocityTracker.getYVelocity();
    373         } else {
    374             return 0f;
    375         }
    376     }
    377 
    378     public void setEnabled(boolean enable) {
    379         mEnabled = enable;
    380     }
    381 
    382     private boolean isEnabled() {
    383         return mEnabled;
    384     }
    385 
    386     private boolean isFullyExpanded(ExpandableView underFocus) {
    387         return underFocus.getIntrinsicHeight() == underFocus.getMaxContentHeight()
    388                 && (!underFocus.isSummaryWithChildren() || underFocus.areChildrenExpanded());
    389     }
    390 
    391     @Override
    392     public boolean onTouchEvent(MotionEvent ev) {
    393         if (!isEnabled()) {
    394             return false;
    395         }
    396         trackVelocity(ev);
    397         final int action = ev.getActionMasked();
    398         if (DEBUG_SCALE) Log.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
    399                 " expanding=" + mExpanding +
    400                 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
    401                 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
    402                 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
    403 
    404         mSGD.onTouchEvent(ev);
    405         final int x = (int) mSGD.getFocusX();
    406         final int y = (int) mSGD.getFocusY();
    407 
    408         if (mOnlyMovements) {
    409             mLastMotionY = ev.getRawY();
    410             return false;
    411         }
    412         switch (action) {
    413             case MotionEvent.ACTION_DOWN:
    414                 mWatchingForPull = mScrollAdapter != null &&
    415                         isInside(mScrollAdapter.getHostView(), x, y);
    416                 mResizedView = findView(x, y);
    417                 mInitialTouchX = ev.getRawX();
    418                 mInitialTouchY = ev.getRawY();
    419                 break;
    420             case MotionEvent.ACTION_MOVE: {
    421                 if (mWatchingForPull) {
    422                     final float yDiff = ev.getRawY() - mInitialTouchY;
    423                     final float xDiff = ev.getRawX() - mInitialTouchX;
    424                     if (yDiff > mTouchSlop && yDiff > Math.abs(xDiff)) {
    425                         if (DEBUG) Log.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
    426                         mWatchingForPull = false;
    427                         if (mResizedView != null && !isFullyExpanded(mResizedView)) {
    428                             if (startExpanding(mResizedView, BLINDS)) {
    429                                 mInitialTouchY = ev.getRawY();
    430                                 mLastMotionY = ev.getRawY();
    431                                 mHasPopped = false;
    432                             }
    433                         }
    434                     }
    435                 }
    436                 if (mExpanding && 0 != (mExpansionStyle & BLINDS)) {
    437                     final float rawHeight = ev.getRawY() - mLastMotionY + mCurrentHeight;
    438                     final float newHeight = clamp(rawHeight);
    439                     boolean isFinished = false;
    440                     boolean expanded = false;
    441                     if (rawHeight > mNaturalHeight) {
    442                         isFinished = true;
    443                         expanded = true;
    444                     }
    445                     if (rawHeight < mSmallSize) {
    446                         isFinished = true;
    447                         expanded = false;
    448                     }
    449 
    450                     if (!mHasPopped) {
    451                         if (mEventSource != null) {
    452                             mEventSource.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
    453                         }
    454                         mHasPopped = true;
    455                     }
    456 
    457                     mScaler.setHeight(newHeight);
    458                     mLastMotionY = ev.getRawY();
    459                     if (isFinished) {
    460                         mCallback.expansionStateChanged(false);
    461                     } else {
    462                         mCallback.expansionStateChanged(true);
    463                     }
    464                     return true;
    465                 }
    466 
    467                 if (mExpanding) {
    468 
    469                     // Gestural expansion is running
    470                     updateExpansion();
    471                     mLastMotionY = ev.getRawY();
    472                     return true;
    473                 }
    474 
    475                 break;
    476             }
    477 
    478             case MotionEvent.ACTION_POINTER_UP:
    479             case MotionEvent.ACTION_POINTER_DOWN:
    480                 if (DEBUG) Log.d(TAG, "pointer change");
    481                 mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
    482                 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
    483                 break;
    484 
    485             case MotionEvent.ACTION_UP:
    486             case MotionEvent.ACTION_CANCEL:
    487                 if (DEBUG) Log.d(TAG, "up/cancel");
    488                 finishExpanding(false, getCurrentVelocity());
    489                 clearView();
    490                 break;
    491         }
    492         mLastMotionY = ev.getRawY();
    493         maybeRecycleVelocityTracker(ev);
    494         return mResizedView != null;
    495     }
    496 
    497     /**
    498      * @return True if the view is expandable, false otherwise.
    499      */
    500     private boolean startExpanding(ExpandableView v, int expandType) {
    501         if (!(v instanceof ExpandableNotificationRow)) {
    502             return false;
    503         }
    504         mExpansionStyle = expandType;
    505         if (mExpanding && v == mResizedView) {
    506             return true;
    507         }
    508         mExpanding = true;
    509         mCallback.expansionStateChanged(true);
    510         if (DEBUG) Log.d(TAG, "scale type " + expandType + " beginning on view: " + v);
    511         mCallback.setUserLockedChild(v, true);
    512         mScaler.setView(v);
    513         mOldHeight = mScaler.getHeight();
    514         mCurrentHeight = mOldHeight;
    515         boolean canBeExpanded = mCallback.canChildBeExpanded(v);
    516         if (canBeExpanded) {
    517             if (DEBUG) Log.d(TAG, "working on an expandable child");
    518             mNaturalHeight = mScaler.getNaturalHeight();
    519             mSmallSize = v.getCollapsedHeight();
    520         } else {
    521             if (DEBUG) Log.d(TAG, "working on a non-expandable child");
    522             mNaturalHeight = mOldHeight;
    523         }
    524         if (DEBUG) Log.d(TAG, "got mOldHeight: " + mOldHeight +
    525                     " mNaturalHeight: " + mNaturalHeight);
    526         return true;
    527     }
    528 
    529     private void finishExpanding(boolean force, float velocity) {
    530         if (!mExpanding) return;
    531 
    532         if (DEBUG) Log.d(TAG, "scale in finishing on view: " + mResizedView);
    533 
    534         float currentHeight = mScaler.getHeight();
    535         float h = mScaler.getHeight();
    536         final boolean wasClosed = (mOldHeight == mSmallSize);
    537         boolean nowExpanded;
    538         int naturalHeight = mScaler.getNaturalHeight();
    539         if (wasClosed) {
    540             nowExpanded = (force || currentHeight > mOldHeight && velocity >= 0);
    541         } else {
    542             nowExpanded = !force && (currentHeight >= mOldHeight || velocity > 0);
    543         }
    544         nowExpanded |= mNaturalHeight == mSmallSize;
    545         if (mScaleAnimation.isRunning()) {
    546             mScaleAnimation.cancel();
    547         }
    548         mCallback.expansionStateChanged(false);
    549         float targetHeight = nowExpanded ? naturalHeight : mSmallSize;
    550         if (targetHeight != currentHeight) {
    551             mScaleAnimation.setFloatValues(targetHeight);
    552             mScaleAnimation.setupStartValues();
    553             final View scaledView = mResizedView;
    554             final boolean expand = nowExpanded;
    555             mScaleAnimation.addListener(new AnimatorListenerAdapter() {
    556                 public boolean mCancelled;
    557 
    558                 @Override
    559                 public void onAnimationEnd(Animator animation) {
    560                     if (!mCancelled) {
    561                         mCallback.setUserExpandedChild(scaledView, expand);
    562                     } else {
    563                         mCallback.setExpansionCancelled(scaledView);
    564                     }
    565                     mCallback.setUserLockedChild(scaledView, false);
    566                     mScaleAnimation.removeListener(this);
    567                 }
    568 
    569                 @Override
    570                 public void onAnimationCancel(Animator animation) {
    571                     mCancelled = true;
    572                 }
    573             });
    574             velocity = nowExpanded == velocity >= 0 ? velocity : 0;
    575             mFlingAnimationUtils.apply(mScaleAnimation, currentHeight, targetHeight, velocity);
    576             mScaleAnimation.start();
    577         } else {
    578             mCallback.setUserExpandedChild(mResizedView, nowExpanded);
    579             mCallback.setUserLockedChild(mResizedView, false);
    580         }
    581 
    582         mExpanding = false;
    583         mExpansionStyle = NONE;
    584 
    585         if (DEBUG) Log.d(TAG, "wasClosed is: " + wasClosed);
    586         if (DEBUG) Log.d(TAG, "currentHeight is: " + currentHeight);
    587         if (DEBUG) Log.d(TAG, "mSmallSize is: " + mSmallSize);
    588         if (DEBUG) Log.d(TAG, "targetHeight is: " + targetHeight);
    589         if (DEBUG) Log.d(TAG, "scale was finished on view: " + mResizedView);
    590     }
    591 
    592     private void clearView() {
    593         mResizedView = null;
    594     }
    595 
    596     /**
    597      * Use this to abort any pending expansions in progress.
    598      */
    599     public void cancel() {
    600         finishExpanding(true, 0f /* velocity */);
    601         clearView();
    602 
    603         // reset the gesture detector
    604         mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
    605     }
    606 
    607     /**
    608      * Change the expansion mode to only observe movements and don't perform any resizing.
    609      * This is needed when the expanding is finished and the scroller kicks in,
    610      * performing an overscroll motion. We only want to shrink it again when we are not
    611      * overscrolled.
    612      *
    613      * @param onlyMovements Should only movements be observed?
    614      */
    615     public void onlyObserveMovements(boolean onlyMovements) {
    616         mOnlyMovements = onlyMovements;
    617     }
    618 }
    619 
    620