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