GestureOverlayView.java revision 27a2b50dedd4315b921d502a30ac0fda51fb6a7c
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 void setGesture(Gesture gesture) {
278        if (mCurrentGesture != null) {
279            clear(false);
280        }
281
282        setCurrentColor(mCertainGestureColor);
283        mCurrentGesture = gesture;
284
285        final Path path = mCurrentGesture.toPath();
286        final RectF bounds = new RectF();
287        path.computeBounds(bounds, true);
288
289        mPath.rewind();
290        mPath.addPath(path, -bounds.left + (getWidth() - bounds.width()) / 2.0f,
291                -bounds.top + (getHeight() - bounds.height()) / 2.0f);
292
293        mResetGesture = true;
294
295        invalidate();
296    }
297
298    public void addOnGestureListener(OnGestureListener listener) {
299        mOnGestureListeners.add(listener);
300    }
301
302    public void removeOnGestureListener(OnGestureListener listener) {
303        mOnGestureListeners.remove(listener);
304    }
305
306    public void removeAllOnGestureListeners() {
307        mOnGestureListeners.clear();
308    }
309
310    public void addOnGesturePerformedListener(OnGesturePerformedListener listener) {
311        mOnGesturePerformedListeners.add(listener);
312        if (mOnGesturePerformedListeners.size() > 0) {
313            mHandleGestureActions = true;
314        }
315    }
316
317    public void removeOnGesturePerformedListener(OnGesturePerformedListener listener) {
318        mOnGesturePerformedListeners.remove(listener);
319        if (mOnGesturePerformedListeners.size() <= 0) {
320            mHandleGestureActions = false;
321        }
322    }
323
324    public void removeAllOnGesturePerformedListeners() {
325        mOnGesturePerformedListeners.clear();
326        mHandleGestureActions = false;
327    }
328
329    public void addOnGesturingListener(OnGesturingListener listener) {
330        mOnGesturingListeners.add(listener);
331    }
332
333    public void removeOnGesturingListener(OnGesturingListener listener) {
334        mOnGesturingListeners.remove(listener);
335    }
336
337    public void removeAllOnGesturingListeners() {
338        mOnGesturingListeners.clear();
339    }
340
341    public boolean isGesturing() {
342        return mIsGesturing;
343    }
344
345    private void setCurrentColor(int color) {
346        mCurrentColor = color;
347        if (mFadingHasStarted) {
348            setPaintAlpha((int) (255 * mFadingAlpha));
349        } else {
350            setPaintAlpha(255);
351        }
352        invalidate();
353    }
354
355    /**
356     * @hide
357     */
358    public Paint getGesturePaint() {
359        return mGesturePaint;
360    }
361
362    @Override
363    public void draw(Canvas canvas) {
364        super.draw(canvas);
365
366        if (mCurrentGesture != null) {
367            canvas.drawPath(mPath, mGesturePaint);
368        }
369    }
370
371    private void setPaintAlpha(int alpha) {
372        alpha += alpha >> 7;
373        final int baseAlpha = mCurrentColor >>> 24;
374        final int useAlpha = baseAlpha * alpha >> 8;
375        mGesturePaint.setColor((mCurrentColor << 8 >>> 8) | (useAlpha << 24));
376    }
377
378    public void clear(boolean animated) {
379        clear(animated, false, true);
380    }
381
382    private void clear(boolean animated, boolean fireActionPerformed, boolean immediate) {
383        setPaintAlpha(255);
384        removeCallbacks(mFadingOut);
385        mResetGesture = false;
386        mFadingOut.fireActionPerformed = fireActionPerformed;
387        mFadingOut.resetMultipleStrokes = false;
388
389        if (animated && mCurrentGesture != null) {
390            mFadingAlpha = 1.0f;
391            mIsFadingOut = true;
392            mFadingHasStarted = false;
393            mFadingStart = AnimationUtils.currentAnimationTimeMillis() + mFadeOffset;
394
395            postDelayed(mFadingOut, mFadeOffset);
396        } else {
397            mFadingAlpha = 1.0f;
398            mIsFadingOut = false;
399            mFadingHasStarted = false;
400
401            if (immediate) {
402                mCurrentGesture = null;
403                mPath.rewind();
404                invalidate();
405            } else if (fireActionPerformed) {
406                postDelayed(mFadingOut, mFadeOffset);
407            } else if (mGestureStrokeType == GESTURE_STROKE_TYPE_MULTIPLE) {
408                mFadingOut.resetMultipleStrokes = true;
409                postDelayed(mFadingOut, mFadeOffset);
410            } else {
411                mCurrentGesture = null;
412                mPath.rewind();
413                invalidate();
414            }
415        }
416    }
417
418    public void cancelClearAnimation() {
419        setPaintAlpha(255);
420        mIsFadingOut = false;
421        mFadingHasStarted = false;
422        removeCallbacks(mFadingOut);
423        mPath.rewind();
424        mCurrentGesture = null;
425    }
426
427    public void cancelGesture() {
428        mIsListeningForGestures = false;
429
430        // add the stroke to the current gesture
431        mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
432
433        // pass the event to handlers
434        final long now = SystemClock.uptimeMillis();
435        final MotionEvent event = MotionEvent.obtain(now, now,
436                MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
437
438        final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
439        int count = listeners.size();
440        for (int i = 0; i < count; i++) {
441            listeners.get(i).onGestureCancelled(this, event);
442        }
443
444        event.recycle();
445
446        clear(false);
447        mIsGesturing = false;
448        mPreviousWasGesturing = false;
449        mStrokeBuffer.clear();
450
451        final ArrayList<OnGesturingListener> otherListeners = mOnGesturingListeners;
452        count = otherListeners.size();
453        for (int i = 0; i < count; i++) {
454            otherListeners.get(i).onGesturingEnded(this);
455        }
456    }
457
458    @Override
459    protected void onDetachedFromWindow() {
460        cancelClearAnimation();
461    }
462
463    @Override
464    public boolean dispatchTouchEvent(MotionEvent event) {
465        if (isEnabled()) {
466            final boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null &&
467                    mCurrentGesture.getStrokesCount() > 0 && mPreviousWasGesturing)) &&
468                    mInterceptEvents;
469
470            processEvent(event);
471
472            if (cancelDispatch) {
473                event.setAction(MotionEvent.ACTION_CANCEL);
474            }
475
476            super.dispatchTouchEvent(event);
477
478            return true;
479        }
480
481        return super.dispatchTouchEvent(event);
482    }
483
484    private boolean processEvent(MotionEvent event) {
485        switch (event.getAction()) {
486            case MotionEvent.ACTION_DOWN:
487                touchDown(event);
488                invalidate();
489                return true;
490            case MotionEvent.ACTION_MOVE:
491                if (mIsListeningForGestures) {
492                    Rect rect = touchMove(event);
493                    if (rect != null) {
494                        invalidate(rect);
495                    }
496                    return true;
497                }
498                break;
499            case MotionEvent.ACTION_UP:
500                if (mIsListeningForGestures) {
501                    touchUp(event, false);
502                    invalidate();
503                    return true;
504                }
505                break;
506            case MotionEvent.ACTION_CANCEL:
507                if (mIsListeningForGestures) {
508                    touchUp(event, true);
509                    invalidate();
510                    return true;
511                }
512        }
513
514        return false;
515    }
516
517    private void touchDown(MotionEvent event) {
518        mIsListeningForGestures = true;
519
520        float x = event.getX();
521        float y = event.getY();
522
523        mX = x;
524        mY = y;
525
526        mTotalLength = 0;
527        mIsGesturing = false;
528
529        if (mGestureStrokeType == GESTURE_STROKE_TYPE_SINGLE || mResetGesture) {
530            if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
531            mResetGesture = false;
532            mCurrentGesture = null;
533            mPath.rewind();
534        } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) {
535            if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
536        }
537
538        // if there is fading out going on, stop it.
539        if (mFadingHasStarted) {
540            cancelClearAnimation();
541        } else if (mIsFadingOut) {
542            setPaintAlpha(255);
543            mIsFadingOut = false;
544            mFadingHasStarted = false;
545            removeCallbacks(mFadingOut);
546        }
547
548        if (mCurrentGesture == null) {
549            mCurrentGesture = new Gesture();
550        }
551
552        mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
553        mPath.moveTo(x, y);
554
555        final int border = mInvalidateExtraBorder;
556        mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border);
557
558        mCurveEndX = x;
559        mCurveEndY = y;
560
561        // pass the event to handlers
562        final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
563        final int count = listeners.size();
564        for (int i = 0; i < count; i++) {
565            listeners.get(i).onGestureStarted(this, event);
566        }
567    }
568
569    private Rect touchMove(MotionEvent event) {
570        Rect areaToRefresh = null;
571
572        final float x = event.getX();
573        final float y = event.getY();
574
575        final float previousX = mX;
576        final float previousY = mY;
577
578        final float dx = Math.abs(x - previousX);
579        final float dy = Math.abs(y - previousY);
580
581        if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) {
582            areaToRefresh = mInvalidRect;
583
584            // start with the curve end
585            final int border = mInvalidateExtraBorder;
586            areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border,
587                    (int) mCurveEndX + border, (int) mCurveEndY + border);
588
589            float cX = mCurveEndX = (x + previousX) / 2;
590            float cY = mCurveEndY = (y + previousY) / 2;
591
592            mPath.quadTo(previousX, previousY, cX, cY);
593
594            // union with the control point of the new curve
595            areaToRefresh.union((int) previousX - border, (int) previousY - border,
596                    (int) previousX + border, (int) previousY + border);
597
598            // union with the end point of the new curve
599            areaToRefresh.union((int) cX - border, (int) cY - border,
600                    (int) cX + border, (int) cY + border);
601
602            mX = x;
603            mY = y;
604
605            mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
606
607            if (mHandleGestureActions && !mIsGesturing) {
608                mTotalLength += (float) Math.sqrt(dx * dx + dy * dy);
609
610                if (mTotalLength > mGestureStrokeLengthThreshold) {
611                    final OrientedBoundingBox box =
612                            GestureUtilities.computeOrientedBoundingBox(mStrokeBuffer);
613
614                    float angle = Math.abs(box.orientation);
615                    if (angle > 90) {
616                        angle = 180 - angle;
617                    }
618
619                    if (box.squareness > mGestureStrokeSquarenessTreshold ||
620                            (mOrientation == ORIENTATION_VERTICAL ?
621                                    angle < mGestureStrokeAngleThreshold :
622                                    angle > mGestureStrokeAngleThreshold)) {
623
624                        mIsGesturing = true;
625                        setCurrentColor(mCertainGestureColor);
626
627                        final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
628                        int count = listeners.size();
629                        for (int i = 0; i < count; i++) {
630                            listeners.get(i).onGesturingStarted(this);
631                        }
632                    }
633                }
634            }
635
636            // pass the event to handlers
637            final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
638            final int count = listeners.size();
639            for (int i = 0; i < count; i++) {
640                listeners.get(i).onGesture(this, event);
641            }
642        }
643
644        return areaToRefresh;
645    }
646
647    private void touchUp(MotionEvent event, boolean cancel) {
648        mIsListeningForGestures = false;
649
650        // A gesture wasn't started or was cancelled
651        if (mCurrentGesture != null) {
652            // add the stroke to the current gesture
653            mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
654
655            if (!cancel) {
656                // pass the event to handlers
657                final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
658                int count = listeners.size();
659                for (int i = 0; i < count; i++) {
660                    listeners.get(i).onGestureEnded(this, event);
661                }
662
663                clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing,
664                        false);
665            } else {
666                cancelGesture(event);
667
668            }
669        } else {
670            cancelGesture(event);
671        }
672
673        mStrokeBuffer.clear();
674        mPreviousWasGesturing = mIsGesturing;
675        mIsGesturing = false;
676
677        final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
678        int count = listeners.size();
679        for (int i = 0; i < count; i++) {
680            listeners.get(i).onGesturingEnded(this);
681        }
682    }
683
684    private void cancelGesture(MotionEvent event) {
685        // pass the event to handlers
686        final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
687        final int count = listeners.size();
688        for (int i = 0; i < count; i++) {
689            listeners.get(i).onGestureCancelled(this, event);
690        }
691
692        clear(false);
693    }
694
695    private void fireOnGesturePerformed() {
696        final ArrayList<OnGesturePerformedListener> actionListeners = mOnGesturePerformedListeners;
697        final int count = actionListeners.size();
698        for (int i = 0; i < count; i++) {
699            actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, mCurrentGesture);
700        }
701    }
702
703    private class FadeOutRunnable implements Runnable {
704        boolean fireActionPerformed;
705        boolean resetMultipleStrokes;
706
707        public void run() {
708            if (mIsFadingOut) {
709                final long now = AnimationUtils.currentAnimationTimeMillis();
710                final long duration = now - mFadingStart;
711
712                if (duration > mFadeDuration) {
713                    if (fireActionPerformed) {
714                        fireOnGesturePerformed();
715                    }
716
717                    mPreviousWasGesturing = false;
718                    mIsFadingOut = false;
719                    mFadingHasStarted = false;
720                    mPath.rewind();
721                    mCurrentGesture = null;
722                    setPaintAlpha(255);
723                } else {
724                    mFadingHasStarted = true;
725                    float interpolatedTime = Math.max(0.0f,
726                            Math.min(1.0f, duration / (float) mFadeDuration));
727                    mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime);
728                    setPaintAlpha((int) (255 * mFadingAlpha));
729                    postDelayed(this, FADE_ANIMATION_RATE);
730                }
731            } else if (resetMultipleStrokes) {
732                mResetGesture = true;
733            } else {
734                fireOnGesturePerformed();
735
736                mFadingHasStarted = false;
737                mPath.rewind();
738                mCurrentGesture = null;
739                mPreviousWasGesturing = false;
740                setPaintAlpha(255);
741            }
742
743            invalidate();
744        }
745    }
746
747    public static interface OnGesturingListener {
748        void onGesturingStarted(GestureOverlayView overlay);
749
750        void onGesturingEnded(GestureOverlayView overlay);
751    }
752
753    public static interface OnGestureListener {
754        void onGestureStarted(GestureOverlayView overlay, MotionEvent event);
755
756        void onGesture(GestureOverlayView overlay, MotionEvent event);
757
758        void onGestureEnded(GestureOverlayView overlay, MotionEvent event);
759
760        void onGestureCancelled(GestureOverlayView overlay, MotionEvent event);
761    }
762
763    public static interface OnGesturePerformedListener {
764        void onGesturePerformed(GestureOverlayView overlay, Gesture gesture);
765    }
766}
767