Home | History | Annotate | Download | only in gesture
      1 /*
      2  * Copyright (C) 2009 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 package android.gesture;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.graphics.Canvas;
     22 import android.graphics.Paint;
     23 import android.graphics.Path;
     24 import android.graphics.Rect;
     25 import android.graphics.RectF;
     26 import android.util.AttributeSet;
     27 import android.view.MotionEvent;
     28 import android.view.animation.AnimationUtils;
     29 import android.view.animation.AccelerateDecelerateInterpolator;
     30 import android.widget.FrameLayout;
     31 import android.os.SystemClock;
     32 import android.annotation.Widget;
     33 import com.android.internal.R;
     34 
     35 import java.util.ArrayList;
     36 
     37 /**
     38  * A transparent overlay for gesture input that can be placed on top of other
     39  * widgets or contain other widgets.
     40  *
     41  * @attr ref android.R.styleable#GestureOverlayView_eventsInterceptionEnabled
     42  * @attr ref android.R.styleable#GestureOverlayView_fadeDuration
     43  * @attr ref android.R.styleable#GestureOverlayView_fadeOffset
     44  * @attr ref android.R.styleable#GestureOverlayView_fadeEnabled
     45  * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeWidth
     46  * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeAngleThreshold
     47  * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeLengthThreshold
     48  * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeSquarenessThreshold
     49  * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeType
     50  * @attr ref android.R.styleable#GestureOverlayView_gestureColor
     51  * @attr ref android.R.styleable#GestureOverlayView_orientation
     52  * @attr ref android.R.styleable#GestureOverlayView_uncertainGestureColor
     53  */
     54 @Widget
     55 public class GestureOverlayView extends FrameLayout {
     56     public static final int GESTURE_STROKE_TYPE_SINGLE = 0;
     57     public static final int GESTURE_STROKE_TYPE_MULTIPLE = 1;
     58 
     59     public static final int ORIENTATION_HORIZONTAL = 0;
     60     public static final int ORIENTATION_VERTICAL = 1;
     61 
     62     private static final int FADE_ANIMATION_RATE = 16;
     63     private static final boolean GESTURE_RENDERING_ANTIALIAS = true;
     64     private static final boolean DITHER_FLAG = true;
     65 
     66     private final Paint mGesturePaint = new Paint();
     67 
     68     private long mFadeDuration = 150;
     69     private long mFadeOffset = 420;
     70     private long mFadingStart;
     71     private boolean mFadingHasStarted;
     72     private boolean mFadeEnabled = true;
     73 
     74     private int mCurrentColor;
     75     private int mCertainGestureColor = 0xFFFFFF00;
     76     private int mUncertainGestureColor = 0x48FFFF00;
     77     private float mGestureStrokeWidth = 12.0f;
     78     private int mInvalidateExtraBorder = 10;
     79 
     80     private int mGestureStrokeType = GESTURE_STROKE_TYPE_SINGLE;
     81     private float mGestureStrokeLengthThreshold = 50.0f;
     82     private float mGestureStrokeSquarenessTreshold = 0.275f;
     83     private float mGestureStrokeAngleThreshold = 40.0f;
     84 
     85     private int mOrientation = ORIENTATION_VERTICAL;
     86 
     87     private final Rect mInvalidRect = new Rect();
     88     private final Path mPath = new Path();
     89     private boolean mGestureVisible = true;
     90 
     91     private float mX;
     92     private float mY;
     93 
     94     private float mCurveEndX;
     95     private float mCurveEndY;
     96 
     97     private float mTotalLength;
     98     private boolean mIsGesturing = false;
     99     private boolean mPreviousWasGesturing = false;
    100     private boolean mInterceptEvents = true;
    101     private boolean mIsListeningForGestures;
    102     private boolean mResetGesture;
    103 
    104     // current gesture
    105     private Gesture mCurrentGesture;
    106     private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);
    107 
    108     // TODO: Make this a list of WeakReferences
    109     private final ArrayList<OnGestureListener> mOnGestureListeners =
    110             new ArrayList<OnGestureListener>();
    111     // TODO: Make this a list of WeakReferences
    112     private final ArrayList<OnGesturePerformedListener> mOnGesturePerformedListeners =
    113             new ArrayList<OnGesturePerformedListener>();
    114     // TODO: Make this a list of WeakReferences
    115     private final ArrayList<OnGesturingListener> mOnGesturingListeners =
    116             new ArrayList<OnGesturingListener>();
    117 
    118     private boolean mHandleGestureActions;
    119 
    120     // fading out effect
    121     private boolean mIsFadingOut = false;
    122     private float mFadingAlpha = 1.0f;
    123     private final AccelerateDecelerateInterpolator mInterpolator =
    124             new AccelerateDecelerateInterpolator();
    125 
    126     private final FadeOutRunnable mFadingOut = new FadeOutRunnable();
    127 
    128     public GestureOverlayView(Context context) {
    129         super(context);
    130         init();
    131     }
    132 
    133     public GestureOverlayView(Context context, AttributeSet attrs) {
    134         this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle);
    135     }
    136 
    137     public GestureOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
    138         this(context, attrs, defStyleAttr, 0);
    139     }
    140 
    141     public GestureOverlayView(
    142             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    143         super(context, attrs, defStyleAttr, defStyleRes);
    144 
    145         final TypedArray a = context.obtainStyledAttributes(
    146                 attrs, R.styleable.GestureOverlayView, defStyleAttr, defStyleRes);
    147 
    148         mGestureStrokeWidth = a.getFloat(R.styleable.GestureOverlayView_gestureStrokeWidth,
    149                 mGestureStrokeWidth);
    150         mInvalidateExtraBorder = Math.max(1, ((int) mGestureStrokeWidth) - 1);
    151         mCertainGestureColor = a.getColor(R.styleable.GestureOverlayView_gestureColor,
    152                 mCertainGestureColor);
    153         mUncertainGestureColor = a.getColor(R.styleable.GestureOverlayView_uncertainGestureColor,
    154                 mUncertainGestureColor);
    155         mFadeDuration = a.getInt(R.styleable.GestureOverlayView_fadeDuration, (int) mFadeDuration);
    156         mFadeOffset = a.getInt(R.styleable.GestureOverlayView_fadeOffset, (int) mFadeOffset);
    157         mGestureStrokeType = a.getInt(R.styleable.GestureOverlayView_gestureStrokeType,
    158                 mGestureStrokeType);
    159         mGestureStrokeLengthThreshold = a.getFloat(
    160                 R.styleable.GestureOverlayView_gestureStrokeLengthThreshold,
    161                 mGestureStrokeLengthThreshold);
    162         mGestureStrokeAngleThreshold = a.getFloat(
    163                 R.styleable.GestureOverlayView_gestureStrokeAngleThreshold,
    164                 mGestureStrokeAngleThreshold);
    165         mGestureStrokeSquarenessTreshold = a.getFloat(
    166                 R.styleable.GestureOverlayView_gestureStrokeSquarenessThreshold,
    167                 mGestureStrokeSquarenessTreshold);
    168         mInterceptEvents = a.getBoolean(R.styleable.GestureOverlayView_eventsInterceptionEnabled,
    169                 mInterceptEvents);
    170         mFadeEnabled = a.getBoolean(R.styleable.GestureOverlayView_fadeEnabled,
    171                 mFadeEnabled);
    172         mOrientation = a.getInt(R.styleable.GestureOverlayView_orientation, mOrientation);
    173 
    174         a.recycle();
    175 
    176         init();
    177     }
    178 
    179     private void init() {
    180         setWillNotDraw(false);
    181 
    182         final Paint gesturePaint = mGesturePaint;
    183         gesturePaint.setAntiAlias(GESTURE_RENDERING_ANTIALIAS);
    184         gesturePaint.setColor(mCertainGestureColor);
    185         gesturePaint.setStyle(Paint.Style.STROKE);
    186         gesturePaint.setStrokeJoin(Paint.Join.ROUND);
    187         gesturePaint.setStrokeCap(Paint.Cap.ROUND);
    188         gesturePaint.setStrokeWidth(mGestureStrokeWidth);
    189         gesturePaint.setDither(DITHER_FLAG);
    190 
    191         mCurrentColor = mCertainGestureColor;
    192         setPaintAlpha(255);
    193     }
    194 
    195     public ArrayList<GesturePoint> getCurrentStroke() {
    196         return mStrokeBuffer;
    197     }
    198 
    199     public int getOrientation() {
    200         return mOrientation;
    201     }
    202 
    203     public void setOrientation(int orientation) {
    204         mOrientation = orientation;
    205     }
    206 
    207     public void setGestureColor(int color) {
    208         mCertainGestureColor = color;
    209     }
    210 
    211     public void setUncertainGestureColor(int color) {
    212         mUncertainGestureColor = color;
    213     }
    214 
    215     public int getUncertainGestureColor() {
    216         return mUncertainGestureColor;
    217     }
    218 
    219     public int getGestureColor() {
    220         return mCertainGestureColor;
    221     }
    222 
    223     public float getGestureStrokeWidth() {
    224         return mGestureStrokeWidth;
    225     }
    226 
    227     public void setGestureStrokeWidth(float gestureStrokeWidth) {
    228         mGestureStrokeWidth = gestureStrokeWidth;
    229         mInvalidateExtraBorder = Math.max(1, ((int) gestureStrokeWidth) - 1);
    230         mGesturePaint.setStrokeWidth(gestureStrokeWidth);
    231     }
    232 
    233     public int getGestureStrokeType() {
    234         return mGestureStrokeType;
    235     }
    236 
    237     public void setGestureStrokeType(int gestureStrokeType) {
    238         mGestureStrokeType = gestureStrokeType;
    239     }
    240 
    241     public float getGestureStrokeLengthThreshold() {
    242         return mGestureStrokeLengthThreshold;
    243     }
    244 
    245     public void setGestureStrokeLengthThreshold(float gestureStrokeLengthThreshold) {
    246         mGestureStrokeLengthThreshold = gestureStrokeLengthThreshold;
    247     }
    248 
    249     public float getGestureStrokeSquarenessTreshold() {
    250         return mGestureStrokeSquarenessTreshold;
    251     }
    252 
    253     public void setGestureStrokeSquarenessTreshold(float gestureStrokeSquarenessTreshold) {
    254         mGestureStrokeSquarenessTreshold = gestureStrokeSquarenessTreshold;
    255     }
    256 
    257     public float getGestureStrokeAngleThreshold() {
    258         return mGestureStrokeAngleThreshold;
    259     }
    260 
    261     public void setGestureStrokeAngleThreshold(float gestureStrokeAngleThreshold) {
    262         mGestureStrokeAngleThreshold = gestureStrokeAngleThreshold;
    263     }
    264 
    265     public boolean isEventsInterceptionEnabled() {
    266         return mInterceptEvents;
    267     }
    268 
    269     public void setEventsInterceptionEnabled(boolean enabled) {
    270         mInterceptEvents = enabled;
    271     }
    272 
    273     public boolean isFadeEnabled() {
    274         return mFadeEnabled;
    275     }
    276 
    277     public void setFadeEnabled(boolean fadeEnabled) {
    278         mFadeEnabled = fadeEnabled;
    279     }
    280 
    281     public Gesture getGesture() {
    282         return mCurrentGesture;
    283     }
    284 
    285     public void setGesture(Gesture gesture) {
    286         if (mCurrentGesture != null) {
    287             clear(false);
    288         }
    289 
    290         setCurrentColor(mCertainGestureColor);
    291         mCurrentGesture = gesture;
    292 
    293         final Path path = mCurrentGesture.toPath();
    294         final RectF bounds = new RectF();
    295         path.computeBounds(bounds, true);
    296 
    297         // TODO: The path should also be scaled to fit inside this view
    298         mPath.rewind();
    299         mPath.addPath(path, -bounds.left + (getWidth() - bounds.width()) / 2.0f,
    300                 -bounds.top + (getHeight() - bounds.height()) / 2.0f);
    301 
    302         mResetGesture = true;
    303 
    304         invalidate();
    305     }
    306 
    307     public Path getGesturePath() {
    308         return mPath;
    309     }
    310 
    311     public Path getGesturePath(Path path) {
    312         path.set(mPath);
    313         return path;
    314     }
    315 
    316     public boolean isGestureVisible() {
    317         return mGestureVisible;
    318     }
    319 
    320     public void setGestureVisible(boolean visible) {
    321         mGestureVisible = visible;
    322     }
    323 
    324     public long getFadeOffset() {
    325         return mFadeOffset;
    326     }
    327 
    328     public void setFadeOffset(long fadeOffset) {
    329         mFadeOffset = fadeOffset;
    330     }
    331 
    332     public void addOnGestureListener(OnGestureListener listener) {
    333         mOnGestureListeners.add(listener);
    334     }
    335 
    336     public void removeOnGestureListener(OnGestureListener listener) {
    337         mOnGestureListeners.remove(listener);
    338     }
    339 
    340     public void removeAllOnGestureListeners() {
    341         mOnGestureListeners.clear();
    342     }
    343 
    344     public void addOnGesturePerformedListener(OnGesturePerformedListener listener) {
    345         mOnGesturePerformedListeners.add(listener);
    346         if (mOnGesturePerformedListeners.size() > 0) {
    347             mHandleGestureActions = true;
    348         }
    349     }
    350 
    351     public void removeOnGesturePerformedListener(OnGesturePerformedListener listener) {
    352         mOnGesturePerformedListeners.remove(listener);
    353         if (mOnGesturePerformedListeners.size() <= 0) {
    354             mHandleGestureActions = false;
    355         }
    356     }
    357 
    358     public void removeAllOnGesturePerformedListeners() {
    359         mOnGesturePerformedListeners.clear();
    360         mHandleGestureActions = false;
    361     }
    362 
    363     public void addOnGesturingListener(OnGesturingListener listener) {
    364         mOnGesturingListeners.add(listener);
    365     }
    366 
    367     public void removeOnGesturingListener(OnGesturingListener listener) {
    368         mOnGesturingListeners.remove(listener);
    369     }
    370 
    371     public void removeAllOnGesturingListeners() {
    372         mOnGesturingListeners.clear();
    373     }
    374 
    375     public boolean isGesturing() {
    376         return mIsGesturing;
    377     }
    378 
    379     private void setCurrentColor(int color) {
    380         mCurrentColor = color;
    381         if (mFadingHasStarted) {
    382             setPaintAlpha((int) (255 * mFadingAlpha));
    383         } else {
    384             setPaintAlpha(255);
    385         }
    386         invalidate();
    387     }
    388 
    389     /**
    390      * @hide
    391      */
    392     public Paint getGesturePaint() {
    393         return mGesturePaint;
    394     }
    395 
    396     @Override
    397     public void draw(Canvas canvas) {
    398         super.draw(canvas);
    399 
    400         if (mCurrentGesture != null && mGestureVisible) {
    401             canvas.drawPath(mPath, mGesturePaint);
    402         }
    403     }
    404 
    405     private void setPaintAlpha(int alpha) {
    406         alpha += alpha >> 7;
    407         final int baseAlpha = mCurrentColor >>> 24;
    408         final int useAlpha = baseAlpha * alpha >> 8;
    409         mGesturePaint.setColor((mCurrentColor << 8 >>> 8) | (useAlpha << 24));
    410     }
    411 
    412     public void clear(boolean animated) {
    413         clear(animated, false, true);
    414     }
    415 
    416     private void clear(boolean animated, boolean fireActionPerformed, boolean immediate) {
    417         setPaintAlpha(255);
    418         removeCallbacks(mFadingOut);
    419         mResetGesture = false;
    420         mFadingOut.fireActionPerformed = fireActionPerformed;
    421         mFadingOut.resetMultipleStrokes = false;
    422 
    423         if (animated && mCurrentGesture != null) {
    424             mFadingAlpha = 1.0f;
    425             mIsFadingOut = true;
    426             mFadingHasStarted = false;
    427             mFadingStart = AnimationUtils.currentAnimationTimeMillis() + mFadeOffset;
    428 
    429             postDelayed(mFadingOut, mFadeOffset);
    430         } else {
    431             mFadingAlpha = 1.0f;
    432             mIsFadingOut = false;
    433             mFadingHasStarted = false;
    434 
    435             if (immediate) {
    436                 mCurrentGesture = null;
    437                 mPath.rewind();
    438                 invalidate();
    439             } else if (fireActionPerformed) {
    440                 postDelayed(mFadingOut, mFadeOffset);
    441             } else if (mGestureStrokeType == GESTURE_STROKE_TYPE_MULTIPLE) {
    442                 mFadingOut.resetMultipleStrokes = true;
    443                 postDelayed(mFadingOut, mFadeOffset);
    444             } else {
    445                 mCurrentGesture = null;
    446                 mPath.rewind();
    447                 invalidate();
    448             }
    449         }
    450     }
    451 
    452     public void cancelClearAnimation() {
    453         setPaintAlpha(255);
    454         mIsFadingOut = false;
    455         mFadingHasStarted = false;
    456         removeCallbacks(mFadingOut);
    457         mPath.rewind();
    458         mCurrentGesture = null;
    459     }
    460 
    461     public void cancelGesture() {
    462         mIsListeningForGestures = false;
    463 
    464         // add the stroke to the current gesture
    465         mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
    466 
    467         // pass the event to handlers
    468         final long now = SystemClock.uptimeMillis();
    469         final MotionEvent event = MotionEvent.obtain(now, now,
    470                 MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
    471 
    472         final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
    473         int count = listeners.size();
    474         for (int i = 0; i < count; i++) {
    475             listeners.get(i).onGestureCancelled(this, event);
    476         }
    477 
    478         event.recycle();
    479 
    480         clear(false);
    481         mIsGesturing = false;
    482         mPreviousWasGesturing = false;
    483         mStrokeBuffer.clear();
    484 
    485         final ArrayList<OnGesturingListener> otherListeners = mOnGesturingListeners;
    486         count = otherListeners.size();
    487         for (int i = 0; i < count; i++) {
    488             otherListeners.get(i).onGesturingEnded(this);
    489         }
    490     }
    491 
    492     @Override
    493     protected void onDetachedFromWindow() {
    494         super.onDetachedFromWindow();
    495         cancelClearAnimation();
    496     }
    497 
    498     @Override
    499     public boolean dispatchTouchEvent(MotionEvent event) {
    500         if (isEnabled()) {
    501             final boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null &&
    502                     mCurrentGesture.getStrokesCount() > 0 && mPreviousWasGesturing)) &&
    503                     mInterceptEvents;
    504 
    505             processEvent(event);
    506 
    507             if (cancelDispatch) {
    508                 event.setAction(MotionEvent.ACTION_CANCEL);
    509             }
    510 
    511             super.dispatchTouchEvent(event);
    512 
    513             return true;
    514         }
    515 
    516         return super.dispatchTouchEvent(event);
    517     }
    518 
    519     private boolean processEvent(MotionEvent event) {
    520         switch (event.getAction()) {
    521             case MotionEvent.ACTION_DOWN:
    522                 touchDown(event);
    523                 invalidate();
    524                 return true;
    525             case MotionEvent.ACTION_MOVE:
    526                 if (mIsListeningForGestures) {
    527                     Rect rect = touchMove(event);
    528                     if (rect != null) {
    529                         invalidate(rect);
    530                     }
    531                     return true;
    532                 }
    533                 break;
    534             case MotionEvent.ACTION_UP:
    535                 if (mIsListeningForGestures) {
    536                     touchUp(event, false);
    537                     invalidate();
    538                     return true;
    539                 }
    540                 break;
    541             case MotionEvent.ACTION_CANCEL:
    542                 if (mIsListeningForGestures) {
    543                     touchUp(event, true);
    544                     invalidate();
    545                     return true;
    546                 }
    547         }
    548 
    549         return false;
    550     }
    551 
    552     private void touchDown(MotionEvent event) {
    553         mIsListeningForGestures = true;
    554 
    555         float x = event.getX();
    556         float y = event.getY();
    557 
    558         mX = x;
    559         mY = y;
    560 
    561         mTotalLength = 0;
    562         mIsGesturing = false;
    563 
    564         if (mGestureStrokeType == GESTURE_STROKE_TYPE_SINGLE || mResetGesture) {
    565             if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
    566             mResetGesture = false;
    567             mCurrentGesture = null;
    568             mPath.rewind();
    569         } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) {
    570             if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
    571         }
    572 
    573         // if there is fading out going on, stop it.
    574         if (mFadingHasStarted) {
    575             cancelClearAnimation();
    576         } else if (mIsFadingOut) {
    577             setPaintAlpha(255);
    578             mIsFadingOut = false;
    579             mFadingHasStarted = false;
    580             removeCallbacks(mFadingOut);
    581         }
    582 
    583         if (mCurrentGesture == null) {
    584             mCurrentGesture = new Gesture();
    585         }
    586 
    587         mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
    588         mPath.moveTo(x, y);
    589 
    590         final int border = mInvalidateExtraBorder;
    591         mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border);
    592 
    593         mCurveEndX = x;
    594         mCurveEndY = y;
    595 
    596         // pass the event to handlers
    597         final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
    598         final int count = listeners.size();
    599         for (int i = 0; i < count; i++) {
    600             listeners.get(i).onGestureStarted(this, event);
    601         }
    602     }
    603 
    604     private Rect touchMove(MotionEvent event) {
    605         Rect areaToRefresh = null;
    606 
    607         final float x = event.getX();
    608         final float y = event.getY();
    609 
    610         final float previousX = mX;
    611         final float previousY = mY;
    612 
    613         final float dx = Math.abs(x - previousX);
    614         final float dy = Math.abs(y - previousY);
    615 
    616         if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) {
    617             areaToRefresh = mInvalidRect;
    618 
    619             // start with the curve end
    620             final int border = mInvalidateExtraBorder;
    621             areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border,
    622                     (int) mCurveEndX + border, (int) mCurveEndY + border);
    623 
    624             float cX = mCurveEndX = (x + previousX) / 2;
    625             float cY = mCurveEndY = (y + previousY) / 2;
    626 
    627             mPath.quadTo(previousX, previousY, cX, cY);
    628 
    629             // union with the control point of the new curve
    630             areaToRefresh.union((int) previousX - border, (int) previousY - border,
    631                     (int) previousX + border, (int) previousY + border);
    632 
    633             // union with the end point of the new curve
    634             areaToRefresh.union((int) cX - border, (int) cY - border,
    635                     (int) cX + border, (int) cY + border);
    636 
    637             mX = x;
    638             mY = y;
    639 
    640             mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
    641 
    642             if (mHandleGestureActions && !mIsGesturing) {
    643                 mTotalLength += (float) Math.sqrt(dx * dx + dy * dy);
    644 
    645                 if (mTotalLength > mGestureStrokeLengthThreshold) {
    646                     final OrientedBoundingBox box =
    647                             GestureUtils.computeOrientedBoundingBox(mStrokeBuffer);
    648 
    649                     float angle = Math.abs(box.orientation);
    650                     if (angle > 90) {
    651                         angle = 180 - angle;
    652                     }
    653 
    654                     if (box.squareness > mGestureStrokeSquarenessTreshold ||
    655                             (mOrientation == ORIENTATION_VERTICAL ?
    656                                     angle < mGestureStrokeAngleThreshold :
    657                                     angle > mGestureStrokeAngleThreshold)) {
    658 
    659                         mIsGesturing = true;
    660                         setCurrentColor(mCertainGestureColor);
    661 
    662                         final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
    663                         int count = listeners.size();
    664                         for (int i = 0; i < count; i++) {
    665                             listeners.get(i).onGesturingStarted(this);
    666                         }
    667                     }
    668                 }
    669             }
    670 
    671             // pass the event to handlers
    672             final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
    673             final int count = listeners.size();
    674             for (int i = 0; i < count; i++) {
    675                 listeners.get(i).onGesture(this, event);
    676             }
    677         }
    678 
    679         return areaToRefresh;
    680     }
    681 
    682     private void touchUp(MotionEvent event, boolean cancel) {
    683         mIsListeningForGestures = false;
    684 
    685         // A gesture wasn't started or was cancelled
    686         if (mCurrentGesture != null) {
    687             // add the stroke to the current gesture
    688             mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
    689 
    690             if (!cancel) {
    691                 // pass the event to handlers
    692                 final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
    693                 int count = listeners.size();
    694                 for (int i = 0; i < count; i++) {
    695                     listeners.get(i).onGestureEnded(this, event);
    696                 }
    697 
    698                 clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing,
    699                         false);
    700             } else {
    701                 cancelGesture(event);
    702 
    703             }
    704         } else {
    705             cancelGesture(event);
    706         }
    707 
    708         mStrokeBuffer.clear();
    709         mPreviousWasGesturing = mIsGesturing;
    710         mIsGesturing = false;
    711 
    712         final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
    713         int count = listeners.size();
    714         for (int i = 0; i < count; i++) {
    715             listeners.get(i).onGesturingEnded(this);
    716         }
    717     }
    718 
    719     private void cancelGesture(MotionEvent event) {
    720         // pass the event to handlers
    721         final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
    722         final int count = listeners.size();
    723         for (int i = 0; i < count; i++) {
    724             listeners.get(i).onGestureCancelled(this, event);
    725         }
    726 
    727         clear(false);
    728     }
    729 
    730     private void fireOnGesturePerformed() {
    731         final ArrayList<OnGesturePerformedListener> actionListeners = mOnGesturePerformedListeners;
    732         final int count = actionListeners.size();
    733         for (int i = 0; i < count; i++) {
    734             actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, mCurrentGesture);
    735         }
    736     }
    737 
    738     private class FadeOutRunnable implements Runnable {
    739         boolean fireActionPerformed;
    740         boolean resetMultipleStrokes;
    741 
    742         public void run() {
    743             if (mIsFadingOut) {
    744                 final long now = AnimationUtils.currentAnimationTimeMillis();
    745                 final long duration = now - mFadingStart;
    746 
    747                 if (duration > mFadeDuration) {
    748                     if (fireActionPerformed) {
    749                         fireOnGesturePerformed();
    750                     }
    751 
    752                     mPreviousWasGesturing = false;
    753                     mIsFadingOut = false;
    754                     mFadingHasStarted = false;
    755                     mPath.rewind();
    756                     mCurrentGesture = null;
    757                     setPaintAlpha(255);
    758                 } else {
    759                     mFadingHasStarted = true;
    760                     float interpolatedTime = Math.max(0.0f,
    761                             Math.min(1.0f, duration / (float) mFadeDuration));
    762                     mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime);
    763                     setPaintAlpha((int) (255 * mFadingAlpha));
    764                     postDelayed(this, FADE_ANIMATION_RATE);
    765                 }
    766             } else if (resetMultipleStrokes) {
    767                 mResetGesture = true;
    768             } else {
    769                 fireOnGesturePerformed();
    770 
    771                 mFadingHasStarted = false;
    772                 mPath.rewind();
    773                 mCurrentGesture = null;
    774                 mPreviousWasGesturing = false;
    775                 setPaintAlpha(255);
    776             }
    777 
    778             invalidate();
    779         }
    780     }
    781 
    782     public static interface OnGesturingListener {
    783         void onGesturingStarted(GestureOverlayView overlay);
    784 
    785         void onGesturingEnded(GestureOverlayView overlay);
    786     }
    787 
    788     public static interface OnGestureListener {
    789         void onGestureStarted(GestureOverlayView overlay, MotionEvent event);
    790 
    791         void onGesture(GestureOverlayView overlay, MotionEvent event);
    792 
    793         void onGestureEnded(GestureOverlayView overlay, MotionEvent event);
    794 
    795         void onGestureCancelled(GestureOverlayView overlay, MotionEvent event);
    796     }
    797 
    798     public static interface OnGesturePerformedListener {
    799         void onGesturePerformed(GestureOverlayView overlay, Gesture gesture);
    800     }
    801 }
    802