OSDN Git Service

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