• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.content.Context;
25 import android.os.Vibrator;
26 import android.util.Slog;
27 import android.view.Gravity;
28 import android.view.MotionEvent;
29 import android.view.ScaleGestureDetector;
30 import android.view.ScaleGestureDetector.OnScaleGestureListener;
31 import android.view.View;
32 import android.view.ViewConfiguration;
33 import android.view.ViewGroup;
34 import android.view.View.OnClickListener;
35 
36 public class ExpandHelper implements Gefingerpoken, OnClickListener {
37     public interface Callback {
getChildAtRawPosition(float x, float y)38         View getChildAtRawPosition(float x, float y);
getChildAtPosition(float x, float y)39         View getChildAtPosition(float x, float y);
canChildBeExpanded(View v)40         boolean canChildBeExpanded(View v);
setUserExpandedChild(View v, boolean userExpanded)41         boolean setUserExpandedChild(View v, boolean userExpanded);
setUserLockedChild(View v, boolean userLocked)42         boolean setUserLockedChild(View v, boolean userLocked);
43     }
44 
45     private static final String TAG = "ExpandHelper";
46     protected static final boolean DEBUG = false;
47     protected static final boolean DEBUG_SCALE = false;
48     protected static final boolean DEBUG_GLOW = false;
49     private static final long EXPAND_DURATION = 250;
50     private static final long GLOW_DURATION = 150;
51 
52     // Set to false to disable focus-based gestures (spread-finger vertical pull).
53     private static final boolean USE_DRAG = true;
54     // Set to false to disable scale-based gestures (both horizontal and vertical).
55     private static final boolean USE_SPAN = true;
56     // Both gestures types may be active at the same time.
57     // At least one gesture type should be active.
58     // A variant of the screwdriver gesture will emerge from either gesture type.
59 
60     // amount of overstretch for maximum brightness expressed in U
61     // 2f: maximum brightness is stretching a 1U to 3U, or a 4U to 6U
62     private static final float STRETCH_INTERVAL = 2f;
63 
64     // level of glow for a touch, without overstretch
65     // overstretch fills the range (GLOW_BASE, 1.0]
66     private static final float GLOW_BASE = 0.5f;
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 View mCurrView;
81     private View mCurrViewTopGlow;
82     private View mCurrViewBottomGlow;
83     private float mOldHeight;
84     private float mNaturalHeight;
85     private float mInitialTouchFocusY;
86     private float mInitialTouchY;
87     private float mInitialTouchSpan;
88     private float mLastFocusY;
89     private float mLastSpanY;
90     private int mTouchSlop;
91     private int mLastMotionY;
92     private float mPopLimit;
93     private int mPopDuration;
94     private float mPullGestureMinXSpan;
95     private Callback mCallback;
96     private ScaleGestureDetector mSGD;
97     private ViewScaler mScaler;
98     private ObjectAnimator mScaleAnimation;
99     private AnimatorSet mGlowAnimationSet;
100     private ObjectAnimator mGlowTopAnimation;
101     private ObjectAnimator mGlowBottomAnimation;
102     private Vibrator mVibrator;
103 
104     private int mSmallSize;
105     private int mLargeSize;
106     private float mMaximumStretch;
107 
108     private int mGravity;
109 
110     private View mScrollView;
111 
112     private OnScaleGestureListener mScaleGestureListener
113             = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
114         @Override
115         public boolean onScaleBegin(ScaleGestureDetector detector) {
116             if (DEBUG_SCALE) Slog.v(TAG, "onscalebegin()");
117             float focusX = detector.getFocusX();
118             float focusY = detector.getFocusY();
119 
120             final View underFocus = findView(focusX, focusY);
121             if (underFocus != null) {
122                 startExpanding(underFocus, STRETCH);
123             }
124             return mExpanding;
125         }
126 
127         @Override
128         public boolean onScale(ScaleGestureDetector detector) {
129             if (DEBUG_SCALE) Slog.v(TAG, "onscale() on " + mCurrView);
130             return true;
131         }
132 
133         @Override
134         public void onScaleEnd(ScaleGestureDetector detector) {
135         }
136     };
137 
138     private class ViewScaler {
139         View mView;
140 
ViewScaler()141         public ViewScaler() {}
setView(View v)142         public void setView(View v) {
143             mView = v;
144         }
setHeight(float h)145         public void setHeight(float h) {
146             if (DEBUG_SCALE) Slog.v(TAG, "SetHeight: setting to " + h);
147             ViewGroup.LayoutParams lp = mView.getLayoutParams();
148             lp.height = (int)h;
149             mView.setLayoutParams(lp);
150             mView.requestLayout();
151         }
getHeight()152         public float getHeight() {
153             int height = mView.getLayoutParams().height;
154             if (height < 0) {
155                 height = mView.getMeasuredHeight();
156             }
157             return height;
158         }
getNaturalHeight(int maximum)159         public int getNaturalHeight(int maximum) {
160             ViewGroup.LayoutParams lp = mView.getLayoutParams();
161             if (DEBUG_SCALE) Slog.v(TAG, "Inspecting a child of type: " +
162                     mView.getClass().getName());
163             int oldHeight = lp.height;
164             lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
165             mView.setLayoutParams(lp);
166             mView.measure(
167                     View.MeasureSpec.makeMeasureSpec(mView.getMeasuredWidth(),
168                                                      View.MeasureSpec.EXACTLY),
169                     View.MeasureSpec.makeMeasureSpec(maximum,
170                                                      View.MeasureSpec.AT_MOST));
171             lp.height = oldHeight;
172             mView.setLayoutParams(lp);
173             return mView.getMeasuredHeight();
174         }
175     }
176 
177     /**
178      * Handle expansion gestures to expand and contract children of the callback.
179      *
180      * @param context application context
181      * @param callback the container that holds the items to be manipulated
182      * @param small the smallest allowable size for the manuipulated items.
183      * @param large the largest allowable size for the manuipulated items.
184      * @param scoller if non-null also manipulate the scroll position to obey the gravity.
185      */
ExpandHelper(Context context, Callback callback, int small, int large)186     public ExpandHelper(Context context, Callback callback, int small, int large) {
187         mSmallSize = small;
188         mMaximumStretch = mSmallSize * STRETCH_INTERVAL;
189         mLargeSize = large;
190         mContext = context;
191         mCallback = callback;
192         mScaler = new ViewScaler();
193         mGravity = Gravity.TOP;
194         mScaleAnimation = ObjectAnimator.ofFloat(mScaler, "height", 0f);
195         mScaleAnimation.setDuration(EXPAND_DURATION);
196         mPopLimit = mContext.getResources().getDimension(R.dimen.blinds_pop_threshold);
197         mPopDuration = mContext.getResources().getInteger(R.integer.blinds_pop_duration_ms);
198         mPullGestureMinXSpan = mContext.getResources().getDimension(R.dimen.pull_span_min);
199 
200         AnimatorListenerAdapter glowVisibilityController = new AnimatorListenerAdapter() {
201             @Override
202             public void onAnimationStart(Animator animation) {
203                 View target = (View) ((ObjectAnimator) animation).getTarget();
204                 if (target.getAlpha() <= 0.0f) {
205                     target.setVisibility(View.VISIBLE);
206                 }
207             }
208 
209             @Override
210             public void onAnimationEnd(Animator animation) {
211                 View target = (View) ((ObjectAnimator) animation).getTarget();
212                 if (target.getAlpha() <= 0.0f) {
213                     target.setVisibility(View.INVISIBLE);
214                 }
215             }
216         };
217 
218         mGlowTopAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
219         mGlowTopAnimation.addListener(glowVisibilityController);
220         mGlowBottomAnimation = ObjectAnimator.ofFloat(null, "alpha", 0f);
221         mGlowBottomAnimation.addListener(glowVisibilityController);
222         mGlowAnimationSet = new AnimatorSet();
223         mGlowAnimationSet.play(mGlowTopAnimation).with(mGlowBottomAnimation);
224         mGlowAnimationSet.setDuration(GLOW_DURATION);
225 
226         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
227         mTouchSlop = configuration.getScaledTouchSlop();
228 
229         mSGD = new ScaleGestureDetector(context, mScaleGestureListener);
230     }
231 
updateExpansion()232     private void updateExpansion() {
233         if (DEBUG_SCALE) Slog.v(TAG, "updateExpansion()");
234         // are we scaling or dragging?
235         float span = mSGD.getCurrentSpan() - mInitialTouchSpan;
236         span *= USE_SPAN ? 1f : 0f;
237         float drag = mSGD.getFocusY() - mInitialTouchFocusY;
238         drag *= USE_DRAG ? 1f : 0f;
239         drag *= mGravity == Gravity.BOTTOM ? -1f : 1f;
240         float pull = Math.abs(drag) + Math.abs(span) + 1f;
241         float hand = drag * Math.abs(drag) / pull + span * Math.abs(span) / pull;
242         float target = hand + mOldHeight;
243         float newHeight = clamp(target);
244         mScaler.setHeight(newHeight);
245 
246         setGlow(calculateGlow(target, newHeight));
247         mLastFocusY = mSGD.getFocusY();
248         mLastSpanY = mSGD.getCurrentSpan();
249     }
250 
clamp(float target)251     private float clamp(float target) {
252         float out = target;
253         out = out < mSmallSize ? mSmallSize : (out > mLargeSize ? mLargeSize : out);
254         out = out > mNaturalHeight ? mNaturalHeight : out;
255         return out;
256     }
257 
findView(float x, float y)258     private View findView(float x, float y) {
259         View v = null;
260         if (mEventSource != null) {
261             int[] location = new int[2];
262             mEventSource.getLocationOnScreen(location);
263             x += location[0];
264             y += location[1];
265             v = mCallback.getChildAtRawPosition(x, y);
266         } else {
267             v = mCallback.getChildAtPosition(x, y);
268         }
269         return v;
270     }
271 
isInside(View v, float x, float y)272     private boolean isInside(View v, float x, float y) {
273         if (DEBUG) Slog.d(TAG, "isinside (" + x + ", " + y + ")");
274 
275         if (v == null) {
276             if (DEBUG) Slog.d(TAG, "isinside null subject");
277             return false;
278         }
279         if (mEventSource != null) {
280             int[] location = new int[2];
281             mEventSource.getLocationOnScreen(location);
282             x += location[0];
283             y += location[1];
284             if (DEBUG) Slog.d(TAG, "  to global (" + x + ", " + y + ")");
285         }
286         int[] location = new int[2];
287         v.getLocationOnScreen(location);
288         x -= location[0];
289         y -= location[1];
290         if (DEBUG) Slog.d(TAG, "  to local (" + x + ", " + y + ")");
291         if (DEBUG) Slog.d(TAG, "  inside (" + v.getWidth() + ", " + v.getHeight() + ")");
292         boolean inside = (x > 0f && y > 0f && x < v.getWidth() & y < v.getHeight());
293         return inside;
294     }
295 
setEventSource(View eventSource)296     public void setEventSource(View eventSource) {
297         mEventSource = eventSource;
298     }
299 
setGravity(int gravity)300     public void setGravity(int gravity) {
301         mGravity = gravity;
302     }
303 
setScrollView(View scrollView)304     public void setScrollView(View scrollView) {
305         mScrollView = scrollView;
306     }
307 
calculateGlow(float target, float actual)308     private float calculateGlow(float target, float actual) {
309         // glow if overscale
310         if (DEBUG_GLOW) Slog.d(TAG, "target: " + target + " actual: " + actual);
311         float stretch = Math.abs((target - actual) / mMaximumStretch);
312         float strength = 1f / (1f + (float) Math.pow(Math.E, -1 * ((8f * stretch) - 5f)));
313         if (DEBUG_GLOW) Slog.d(TAG, "stretch: " + stretch + " strength: " + strength);
314         return (GLOW_BASE + strength * (1f - GLOW_BASE));
315     }
316 
setGlow(float glow)317     public void setGlow(float glow) {
318         if (!mGlowAnimationSet.isRunning() || glow == 0f) {
319             if (mGlowAnimationSet.isRunning()) {
320                 mGlowAnimationSet.end();
321             }
322             if (mCurrViewTopGlow != null && mCurrViewBottomGlow != null) {
323                 if (glow == 0f || mCurrViewTopGlow.getAlpha() == 0f) {
324                     // animate glow in and out
325                     mGlowTopAnimation.setTarget(mCurrViewTopGlow);
326                     mGlowBottomAnimation.setTarget(mCurrViewBottomGlow);
327                     mGlowTopAnimation.setFloatValues(glow);
328                     mGlowBottomAnimation.setFloatValues(glow);
329                     mGlowAnimationSet.setupStartValues();
330                     mGlowAnimationSet.start();
331                 } else {
332                     // set it explicitly in reponse to touches.
333                     mCurrViewTopGlow.setAlpha(glow);
334                     mCurrViewBottomGlow.setAlpha(glow);
335                     handleGlowVisibility();
336                 }
337             }
338         }
339     }
340 
handleGlowVisibility()341     private void handleGlowVisibility() {
342         mCurrViewTopGlow.setVisibility(mCurrViewTopGlow.getAlpha() <= 0.0f ?
343                 View.INVISIBLE : View.VISIBLE);
344         mCurrViewBottomGlow.setVisibility(mCurrViewBottomGlow.getAlpha() <= 0.0f ?
345                 View.INVISIBLE : View.VISIBLE);
346     }
347 
348     @Override
onInterceptTouchEvent(MotionEvent ev)349     public boolean onInterceptTouchEvent(MotionEvent ev) {
350         final int action = ev.getAction();
351         if (DEBUG_SCALE) Slog.d(TAG, "intercept: act=" + MotionEvent.actionToString(action) +
352                          " expanding=" + mExpanding +
353                          (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
354                          (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
355                          (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
356         // check for a spread-finger vertical pull gesture
357         mSGD.onTouchEvent(ev);
358         final int x = (int) mSGD.getFocusX();
359         final int y = (int) mSGD.getFocusY();
360 
361         mInitialTouchFocusY = y;
362         mInitialTouchSpan = mSGD.getCurrentSpan();
363         mLastFocusY = mInitialTouchFocusY;
364         mLastSpanY = mInitialTouchSpan;
365         if (DEBUG_SCALE) Slog.d(TAG, "set initial span: " + mInitialTouchSpan);
366 
367         if (mExpanding) {
368             return true;
369         } else {
370             if ((action == MotionEvent.ACTION_MOVE) && 0 != (mExpansionStyle & BLINDS)) {
371                 // we've begun Venetian blinds style expansion
372                 return true;
373             }
374             final float xspan = mSGD.getCurrentSpanX();
375             if ((action == MotionEvent.ACTION_MOVE &&
376                     xspan > mPullGestureMinXSpan &&
377                     xspan > mSGD.getCurrentSpanY())) {
378                 // detect a vertical pulling gesture with fingers somewhat separated
379                 if (DEBUG_SCALE) Slog.v(TAG, "got pull gesture (xspan=" + xspan + "px)");
380 
381                 final View underFocus = findView(x, y);
382                 if (underFocus != null) {
383                     startExpanding(underFocus, PULL);
384                 }
385                 return true;
386             }
387             if (mScrollView != null && mScrollView.getScrollY() > 0) {
388                 return false;
389             }
390             // Now look for other gestures
391             switch (action & MotionEvent.ACTION_MASK) {
392             case MotionEvent.ACTION_MOVE: {
393                 if (mWatchingForPull) {
394                     final int yDiff = y - mLastMotionY;
395                     if (yDiff > mTouchSlop) {
396                         if (DEBUG) Slog.v(TAG, "got venetian gesture (dy=" + yDiff + "px)");
397                         mLastMotionY = y;
398                         final View underFocus = findView(x, y);
399                         if (underFocus != null) {
400                             startExpanding(underFocus, BLINDS);
401                             mInitialTouchY = mLastMotionY;
402                             mHasPopped = false;
403                         }
404                     }
405                 }
406                 break;
407             }
408 
409             case MotionEvent.ACTION_DOWN:
410                 mWatchingForPull = isInside(mScrollView, x, y);
411                 mLastMotionY = y;
412                 break;
413 
414             case MotionEvent.ACTION_CANCEL:
415             case MotionEvent.ACTION_UP:
416                 if (DEBUG) Slog.d(TAG, "up/cancel");
417                 finishExpanding(false);
418                 clearView();
419                 break;
420             }
421             return mExpanding;
422         }
423     }
424 
425     @Override
onTouchEvent(MotionEvent ev)426     public boolean onTouchEvent(MotionEvent ev) {
427         final int action = ev.getActionMasked();
428         if (DEBUG_SCALE) Slog.d(TAG, "touch: act=" + MotionEvent.actionToString(action) +
429                 " expanding=" + mExpanding +
430                 (0 != (mExpansionStyle & BLINDS) ? " (blinds)" : "") +
431                 (0 != (mExpansionStyle & PULL) ? " (pull)" : "") +
432                 (0 != (mExpansionStyle & STRETCH) ? " (stretch)" : ""));
433 
434         mSGD.onTouchEvent(ev);
435 
436         switch (action) {
437             case MotionEvent.ACTION_MOVE: {
438                 if (0 != (mExpansionStyle & BLINDS)) {
439                     final float rawHeight = ev.getY() - mInitialTouchY + mOldHeight;
440                     final float newHeight = clamp(rawHeight);
441                     final boolean wasClosed = (mOldHeight == mSmallSize);
442                     boolean isFinished = false;
443                     if (rawHeight > mNaturalHeight) {
444                         isFinished = true;
445                     }
446                     if (rawHeight < mSmallSize) {
447                         isFinished = true;
448                     }
449 
450                     final float pull = Math.abs(ev.getY() - mInitialTouchY);
451                     if (mHasPopped || pull > mPopLimit) {
452                         if (!mHasPopped) {
453                             vibrate(mPopDuration);
454                             mHasPopped = true;
455                         }
456                     }
457 
458                     if (mHasPopped) {
459                         mScaler.setHeight(newHeight);
460                         setGlow(GLOW_BASE);
461                     } else {
462                         setGlow(calculateGlow(4f * pull, 0f));
463                     }
464 
465                     final int x = (int) mSGD.getFocusX();
466                     final int y = (int) mSGD.getFocusY();
467                     View underFocus = findView(x, y);
468                     if (isFinished && underFocus != null && underFocus != mCurrView) {
469                         finishExpanding(false); // @@@ needed?
470                         startExpanding(underFocus, BLINDS);
471                         mInitialTouchY = y;
472                         mHasPopped = false;
473                     }
474                     return true;
475                 }
476 
477                 if (mExpanding) {
478                     updateExpansion();
479                     return true;
480                 }
481 
482                 break;
483             }
484 
485             case MotionEvent.ACTION_POINTER_UP:
486             case MotionEvent.ACTION_POINTER_DOWN:
487                 if (DEBUG) Slog.d(TAG, "pointer change");
488                 mInitialTouchY += mSGD.getFocusY() - mLastFocusY;
489                 mInitialTouchSpan += mSGD.getCurrentSpan() - mLastSpanY;
490                 break;
491 
492             case MotionEvent.ACTION_UP:
493             case MotionEvent.ACTION_CANCEL:
494                 if (DEBUG) Slog.d(TAG, "up/cancel");
495                 finishExpanding(false);
496                 clearView();
497                 break;
498         }
499         return true;
500     }
501 
startExpanding(View v, int expandType)502     private void startExpanding(View v, int expandType) {
503         mExpansionStyle = expandType;
504         if (mExpanding &&  v == mCurrView) {
505             return;
506         }
507         mExpanding = true;
508         if (DEBUG) Slog.d(TAG, "scale type " + expandType + " beginning on view: " + v);
509         mCallback.setUserLockedChild(v, true);
510         setView(v);
511         setGlow(GLOW_BASE);
512         mScaler.setView(v);
513         mOldHeight = mScaler.getHeight();
514         if (mCallback.canChildBeExpanded(v)) {
515             if (DEBUG) Slog.d(TAG, "working on an expandable child");
516             mNaturalHeight = mScaler.getNaturalHeight(mLargeSize);
517         } else {
518             if (DEBUG) Slog.d(TAG, "working on a non-expandable child");
519             mNaturalHeight = mOldHeight;
520         }
521         if (DEBUG) Slog.d(TAG, "got mOldHeight: " + mOldHeight +
522                     " mNaturalHeight: " + mNaturalHeight);
523         v.getParent().requestDisallowInterceptTouchEvent(true);
524     }
525 
finishExpanding(boolean force)526     private void finishExpanding(boolean force) {
527         if (!mExpanding) return;
528 
529         if (DEBUG) Slog.d(TAG, "scale in finishing on view: " + mCurrView);
530 
531         float currentHeight = mScaler.getHeight();
532         float targetHeight = mSmallSize;
533         float h = mScaler.getHeight();
534         final boolean wasClosed = (mOldHeight == mSmallSize);
535         if (wasClosed) {
536             targetHeight = (force || currentHeight > mSmallSize) ? mNaturalHeight : mSmallSize;
537         } else {
538             targetHeight = (force || currentHeight < mNaturalHeight) ? mSmallSize : mNaturalHeight;
539         }
540         if (mScaleAnimation.isRunning()) {
541             mScaleAnimation.cancel();
542         }
543         setGlow(0f);
544         mCallback.setUserExpandedChild(mCurrView, h == mNaturalHeight);
545         if (targetHeight != currentHeight) {
546             mScaleAnimation.setFloatValues(targetHeight);
547             mScaleAnimation.setupStartValues();
548             mScaleAnimation.start();
549         }
550         mCallback.setUserLockedChild(mCurrView, false);
551 
552         mExpanding = false;
553         mExpansionStyle = NONE;
554 
555         if (DEBUG) Slog.d(TAG, "wasClosed is: " + wasClosed);
556         if (DEBUG) Slog.d(TAG, "currentHeight is: " + currentHeight);
557         if (DEBUG) Slog.d(TAG, "mSmallSize is: " + mSmallSize);
558         if (DEBUG) Slog.d(TAG, "targetHeight is: " + targetHeight);
559         if (DEBUG) Slog.d(TAG, "scale was finished on view: " + mCurrView);
560     }
561 
clearView()562     private void clearView() {
563         mCurrView = null;
564         mCurrViewTopGlow = null;
565         mCurrViewBottomGlow = null;
566     }
567 
setView(View v)568     private void setView(View v) {
569         mCurrView = v;
570         if (v instanceof ViewGroup) {
571             ViewGroup g = (ViewGroup) v;
572             mCurrViewTopGlow = g.findViewById(R.id.top_glow);
573             mCurrViewBottomGlow = g.findViewById(R.id.bottom_glow);
574             if (DEBUG) {
575                 String debugLog = "Looking for glows: " +
576                         (mCurrViewTopGlow != null ? "found top " : "didn't find top") +
577                         (mCurrViewBottomGlow != null ? "found bottom " : "didn't find bottom");
578                 Slog.v(TAG,  debugLog);
579             }
580         }
581     }
582 
583     @Override
onClick(View v)584     public void onClick(View v) {
585         startExpanding(v, STRETCH);
586         finishExpanding(true);
587         clearView();
588     }
589 
590     /**
591      * Use this to abort any pending expansions in progress.
592      */
cancel()593     public void cancel() {
594         finishExpanding(true);
595         clearView();
596 
597         // reset the gesture detector
598         mSGD = new ScaleGestureDetector(mContext, mScaleGestureListener);
599     }
600 
601     /**
602      * Triggers haptic feedback.
603      */
vibrate(long duration)604     private synchronized void vibrate(long duration) {
605         if (mVibrator == null) {
606             mVibrator = (android.os.Vibrator)
607                     mContext.getSystemService(Context.VIBRATOR_SERVICE);
608         }
609         mVibrator.vibrate(duration);
610     }
611 }
612 
613