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