GestureOverlayView.java revision 33253a4baa6279f81a73425b49dfb6abe5f5416e
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        super.onDetachedFromWindow();
490        cancelClearAnimation();
491    }
492
493    @Override
494    public boolean dispatchTouchEvent(MotionEvent event) {
495        if (isEnabled()) {
496            final boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null &&
497                    mCurrentGesture.getStrokesCount() > 0 && mPreviousWasGesturing)) &&
498                    mInterceptEvents;
499
500            processEvent(event);
501
502            if (cancelDispatch) {
503                event.setAction(MotionEvent.ACTION_CANCEL);
504            }
505
506            super.dispatchTouchEvent(event);
507
508            return true;
509        }
510
511        return super.dispatchTouchEvent(event);
512    }
513
514    private boolean processEvent(MotionEvent event) {
515        switch (event.getAction()) {
516            case MotionEvent.ACTION_DOWN:
517                touchDown(event);
518                invalidate();
519                return true;
520            case MotionEvent.ACTION_MOVE:
521                if (mIsListeningForGestures) {
522                    Rect rect = touchMove(event);
523                    if (rect != null) {
524                        invalidate(rect);
525                    }
526                    return true;
527                }
528                break;
529            case MotionEvent.ACTION_UP:
530                if (mIsListeningForGestures) {
531                    touchUp(event, false);
532                    invalidate();
533                    return true;
534                }
535                break;
536            case MotionEvent.ACTION_CANCEL:
537                if (mIsListeningForGestures) {
538                    touchUp(event, true);
539                    invalidate();
540                    return true;
541                }
542        }
543
544        return false;
545    }
546
547    private void touchDown(MotionEvent event) {
548        mIsListeningForGestures = true;
549
550        float x = event.getX();
551        float y = event.getY();
552
553        mX = x;
554        mY = y;
555
556        mTotalLength = 0;
557        mIsGesturing = false;
558
559        if (mGestureStrokeType == GESTURE_STROKE_TYPE_SINGLE || mResetGesture) {
560            if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
561            mResetGesture = false;
562            mCurrentGesture = null;
563            mPath.rewind();
564        } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) {
565            if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
566        }
567
568        // if there is fading out going on, stop it.
569        if (mFadingHasStarted) {
570            cancelClearAnimation();
571        } else if (mIsFadingOut) {
572            setPaintAlpha(255);
573            mIsFadingOut = false;
574            mFadingHasStarted = false;
575            removeCallbacks(mFadingOut);
576        }
577
578        if (mCurrentGesture == null) {
579            mCurrentGesture = new Gesture();
580        }
581
582        mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
583        mPath.moveTo(x, y);
584
585        final int border = mInvalidateExtraBorder;
586        mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border);
587
588        mCurveEndX = x;
589        mCurveEndY = y;
590
591        // pass the event to handlers
592        final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
593        final int count = listeners.size();
594        for (int i = 0; i < count; i++) {
595            listeners.get(i).onGestureStarted(this, event);
596        }
597    }
598
599    private Rect touchMove(MotionEvent event) {
600        Rect areaToRefresh = null;
601
602        final float x = event.getX();
603        final float y = event.getY();
604
605        final float previousX = mX;
606        final float previousY = mY;
607
608        final float dx = Math.abs(x - previousX);
609        final float dy = Math.abs(y - previousY);
610
611        if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) {
612            areaToRefresh = mInvalidRect;
613
614            // start with the curve end
615            final int border = mInvalidateExtraBorder;
616            areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border,
617                    (int) mCurveEndX + border, (int) mCurveEndY + border);
618
619            float cX = mCurveEndX = (x + previousX) / 2;
620            float cY = mCurveEndY = (y + previousY) / 2;
621
622            mPath.quadTo(previousX, previousY, cX, cY);
623
624            // union with the control point of the new curve
625            areaToRefresh.union((int) previousX - border, (int) previousY - border,
626                    (int) previousX + border, (int) previousY + border);
627
628            // union with the end point of the new curve
629            areaToRefresh.union((int) cX - border, (int) cY - border,
630                    (int) cX + border, (int) cY + border);
631
632            mX = x;
633            mY = y;
634
635            mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
636
637            if (mHandleGestureActions && !mIsGesturing) {
638                mTotalLength += (float) Math.hypot(dx, dy);
639
640                if (mTotalLength > mGestureStrokeLengthThreshold) {
641                    final OrientedBoundingBox box =
642                            GestureUtils.computeOrientedBoundingBox(mStrokeBuffer);
643
644                    float angle = Math.abs(box.orientation);
645                    if (angle > 90) {
646                        angle = 180 - angle;
647                    }
648
649                    if (box.squareness > mGestureStrokeSquarenessTreshold ||
650                            (mOrientation == ORIENTATION_VERTICAL ?
651                                    angle < mGestureStrokeAngleThreshold :
652                                    angle > mGestureStrokeAngleThreshold)) {
653
654                        mIsGesturing = true;
655                        setCurrentColor(mCertainGestureColor);
656
657                        final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
658                        int count = listeners.size();
659                        for (int i = 0; i < count; i++) {
660                            listeners.get(i).onGesturingStarted(this);
661                        }
662                    }
663                }
664            }
665
666            // pass the event to handlers
667            final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
668            final int count = listeners.size();
669            for (int i = 0; i < count; i++) {
670                listeners.get(i).onGesture(this, event);
671            }
672        }
673
674        return areaToRefresh;
675    }
676
677    private void touchUp(MotionEvent event, boolean cancel) {
678        mIsListeningForGestures = false;
679
680        // A gesture wasn't started or was cancelled
681        if (mCurrentGesture != null) {
682            // add the stroke to the current gesture
683            mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
684
685            if (!cancel) {
686                // pass the event to handlers
687                final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
688                int count = listeners.size();
689                for (int i = 0; i < count; i++) {
690                    listeners.get(i).onGestureEnded(this, event);
691                }
692
693                clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing,
694                        false);
695            } else {
696                cancelGesture(event);
697
698            }
699        } else {
700            cancelGesture(event);
701        }
702
703        mStrokeBuffer.clear();
704        mPreviousWasGesturing = mIsGesturing;
705        mIsGesturing = false;
706
707        final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
708        int count = listeners.size();
709        for (int i = 0; i < count; i++) {
710            listeners.get(i).onGesturingEnded(this);
711        }
712    }
713
714    private void cancelGesture(MotionEvent event) {
715        // pass the event to handlers
716        final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
717        final int count = listeners.size();
718        for (int i = 0; i < count; i++) {
719            listeners.get(i).onGestureCancelled(this, event);
720        }
721
722        clear(false);
723    }
724
725    private void fireOnGesturePerformed() {
726        final ArrayList<OnGesturePerformedListener> actionListeners = mOnGesturePerformedListeners;
727        final int count = actionListeners.size();
728        for (int i = 0; i < count; i++) {
729            actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, mCurrentGesture);
730        }
731    }
732
733    private class FadeOutRunnable implements Runnable {
734        boolean fireActionPerformed;
735        boolean resetMultipleStrokes;
736
737        public void run() {
738            if (mIsFadingOut) {
739                final long now = AnimationUtils.currentAnimationTimeMillis();
740                final long duration = now - mFadingStart;
741
742                if (duration > mFadeDuration) {
743                    if (fireActionPerformed) {
744                        fireOnGesturePerformed();
745                    }
746
747                    mPreviousWasGesturing = false;
748                    mIsFadingOut = false;
749                    mFadingHasStarted = false;
750                    mPath.rewind();
751                    mCurrentGesture = null;
752                    setPaintAlpha(255);
753                } else {
754                    mFadingHasStarted = true;
755                    float interpolatedTime = Math.max(0.0f,
756                            Math.min(1.0f, duration / (float) mFadeDuration));
757                    mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime);
758                    setPaintAlpha((int) (255 * mFadingAlpha));
759                    postDelayed(this, FADE_ANIMATION_RATE);
760                }
761            } else if (resetMultipleStrokes) {
762                mResetGesture = true;
763            } else {
764                fireOnGesturePerformed();
765
766                mFadingHasStarted = false;
767                mPath.rewind();
768                mCurrentGesture = null;
769                mPreviousWasGesturing = false;
770                setPaintAlpha(255);
771            }
772
773            invalidate();
774        }
775    }
776
777    public static interface OnGesturingListener {
778        void onGesturingStarted(GestureOverlayView overlay);
779
780        void onGesturingEnded(GestureOverlayView overlay);
781    }
782
783    public static interface OnGestureListener {
784        void onGestureStarted(GestureOverlayView overlay, MotionEvent event);
785
786        void onGesture(GestureOverlayView overlay, MotionEvent event);
787
788        void onGestureEnded(GestureOverlayView overlay, MotionEvent event);
789
790        void onGestureCancelled(GestureOverlayView overlay, MotionEvent event);
791    }
792
793    public static interface OnGesturePerformedListener {
794        void onGesturePerformed(GestureOverlayView overlay, Gesture gesture);
795    }
796}
797