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.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 {
     38         View getChildAtRawPosition(float x, float y);
     39         View getChildAtPosition(float x, float y);
     40         boolean canChildBeExpanded(View v);
     41         boolean setUserExpandedChild(View v, boolean userExpanded);
     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 
    141         public ViewScaler() {}
    142         public void setView(View v) {
    143             mView = v;
    144         }
    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         }
    152         public float getHeight() {
    153             int height = mView.getLayoutParams().height;
    154             if (height < 0) {
    155                 height = mView.getMeasuredHeight();
    156             }
    157             return height;
    158         }
    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      */
    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 
    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 
    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 
    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 
    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 
    296     public void setEventSource(View eventSource) {
    297         mEventSource = eventSource;
    298     }
    299 
    300     public void setGravity(int gravity) {
    301         mGravity = gravity;
    302     }
    303 
    304     public void setScrollView(View scrollView) {
    305         mScrollView = scrollView;
    306     }
    307 
    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 
    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 
    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
    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
    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 
    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 
    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 
    562     private void clearView() {
    563         mCurrView = null;
    564         mCurrViewTopGlow = null;
    565         mCurrViewBottomGlow = null;
    566     }
    567 
    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
    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      */
    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      */
    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