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