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 
17 package android.gesture;
18 
19 import android.annotation.ColorInt;
20 import android.content.Context;
21 import android.content.res.TypedArray;
22 import android.graphics.Canvas;
23 import android.graphics.Paint;
24 import android.graphics.Path;
25 import android.graphics.Rect;
26 import android.graphics.RectF;
27 import android.util.AttributeSet;
28 import android.view.MotionEvent;
29 import android.view.animation.AnimationUtils;
30 import android.view.animation.AccelerateDecelerateInterpolator;
31 import android.widget.FrameLayout;
32 import android.os.SystemClock;
33 import android.annotation.Widget;
34 import com.android.internal.R;
35 
36 import 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
56 public 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 
GestureOverlayView(Context context)129     public GestureOverlayView(Context context) {
130         super(context);
131         init();
132     }
133 
GestureOverlayView(Context context, AttributeSet attrs)134     public GestureOverlayView(Context context, AttributeSet attrs) {
135         this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle);
136     }
137 
GestureOverlayView(Context context, AttributeSet attrs, int defStyleAttr)138     public GestureOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
139         this(context, attrs, defStyleAttr, 0);
140     }
141 
GestureOverlayView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 
init()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 
getCurrentStroke()196     public ArrayList<GesturePoint> getCurrentStroke() {
197         return mStrokeBuffer;
198     }
199 
getOrientation()200     public int getOrientation() {
201         return mOrientation;
202     }
203 
setOrientation(int orientation)204     public void setOrientation(int orientation) {
205         mOrientation = orientation;
206     }
207 
setGestureColor(@olorInt int color)208     public void setGestureColor(@ColorInt int color) {
209         mCertainGestureColor = color;
210     }
211 
setUncertainGestureColor(@olorInt int color)212     public void setUncertainGestureColor(@ColorInt int color) {
213         mUncertainGestureColor = color;
214     }
215 
216     @ColorInt
getUncertainGestureColor()217     public int getUncertainGestureColor() {
218         return mUncertainGestureColor;
219     }
220 
221     @ColorInt
getGestureColor()222     public int getGestureColor() {
223         return mCertainGestureColor;
224     }
225 
getGestureStrokeWidth()226     public float getGestureStrokeWidth() {
227         return mGestureStrokeWidth;
228     }
229 
setGestureStrokeWidth(float gestureStrokeWidth)230     public void setGestureStrokeWidth(float gestureStrokeWidth) {
231         mGestureStrokeWidth = gestureStrokeWidth;
232         mInvalidateExtraBorder = Math.max(1, ((int) gestureStrokeWidth) - 1);
233         mGesturePaint.setStrokeWidth(gestureStrokeWidth);
234     }
235 
getGestureStrokeType()236     public int getGestureStrokeType() {
237         return mGestureStrokeType;
238     }
239 
setGestureStrokeType(int gestureStrokeType)240     public void setGestureStrokeType(int gestureStrokeType) {
241         mGestureStrokeType = gestureStrokeType;
242     }
243 
getGestureStrokeLengthThreshold()244     public float getGestureStrokeLengthThreshold() {
245         return mGestureStrokeLengthThreshold;
246     }
247 
setGestureStrokeLengthThreshold(float gestureStrokeLengthThreshold)248     public void setGestureStrokeLengthThreshold(float gestureStrokeLengthThreshold) {
249         mGestureStrokeLengthThreshold = gestureStrokeLengthThreshold;
250     }
251 
getGestureStrokeSquarenessTreshold()252     public float getGestureStrokeSquarenessTreshold() {
253         return mGestureStrokeSquarenessTreshold;
254     }
255 
setGestureStrokeSquarenessTreshold(float gestureStrokeSquarenessTreshold)256     public void setGestureStrokeSquarenessTreshold(float gestureStrokeSquarenessTreshold) {
257         mGestureStrokeSquarenessTreshold = gestureStrokeSquarenessTreshold;
258     }
259 
getGestureStrokeAngleThreshold()260     public float getGestureStrokeAngleThreshold() {
261         return mGestureStrokeAngleThreshold;
262     }
263 
setGestureStrokeAngleThreshold(float gestureStrokeAngleThreshold)264     public void setGestureStrokeAngleThreshold(float gestureStrokeAngleThreshold) {
265         mGestureStrokeAngleThreshold = gestureStrokeAngleThreshold;
266     }
267 
isEventsInterceptionEnabled()268     public boolean isEventsInterceptionEnabled() {
269         return mInterceptEvents;
270     }
271 
setEventsInterceptionEnabled(boolean enabled)272     public void setEventsInterceptionEnabled(boolean enabled) {
273         mInterceptEvents = enabled;
274     }
275 
isFadeEnabled()276     public boolean isFadeEnabled() {
277         return mFadeEnabled;
278     }
279 
setFadeEnabled(boolean fadeEnabled)280     public void setFadeEnabled(boolean fadeEnabled) {
281         mFadeEnabled = fadeEnabled;
282     }
283 
getGesture()284     public Gesture getGesture() {
285         return mCurrentGesture;
286     }
287 
setGesture(Gesture gesture)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 
getGesturePath()310     public Path getGesturePath() {
311         return mPath;
312     }
313 
getGesturePath(Path path)314     public Path getGesturePath(Path path) {
315         path.set(mPath);
316         return path;
317     }
318 
isGestureVisible()319     public boolean isGestureVisible() {
320         return mGestureVisible;
321     }
322 
setGestureVisible(boolean visible)323     public void setGestureVisible(boolean visible) {
324         mGestureVisible = visible;
325     }
326 
getFadeOffset()327     public long getFadeOffset() {
328         return mFadeOffset;
329     }
330 
setFadeOffset(long fadeOffset)331     public void setFadeOffset(long fadeOffset) {
332         mFadeOffset = fadeOffset;
333     }
334 
addOnGestureListener(OnGestureListener listener)335     public void addOnGestureListener(OnGestureListener listener) {
336         mOnGestureListeners.add(listener);
337     }
338 
removeOnGestureListener(OnGestureListener listener)339     public void removeOnGestureListener(OnGestureListener listener) {
340         mOnGestureListeners.remove(listener);
341     }
342 
removeAllOnGestureListeners()343     public void removeAllOnGestureListeners() {
344         mOnGestureListeners.clear();
345     }
346 
addOnGesturePerformedListener(OnGesturePerformedListener listener)347     public void addOnGesturePerformedListener(OnGesturePerformedListener listener) {
348         mOnGesturePerformedListeners.add(listener);
349         if (mOnGesturePerformedListeners.size() > 0) {
350             mHandleGestureActions = true;
351         }
352     }
353 
removeOnGesturePerformedListener(OnGesturePerformedListener listener)354     public void removeOnGesturePerformedListener(OnGesturePerformedListener listener) {
355         mOnGesturePerformedListeners.remove(listener);
356         if (mOnGesturePerformedListeners.size() <= 0) {
357             mHandleGestureActions = false;
358         }
359     }
360 
removeAllOnGesturePerformedListeners()361     public void removeAllOnGesturePerformedListeners() {
362         mOnGesturePerformedListeners.clear();
363         mHandleGestureActions = false;
364     }
365 
addOnGesturingListener(OnGesturingListener listener)366     public void addOnGesturingListener(OnGesturingListener listener) {
367         mOnGesturingListeners.add(listener);
368     }
369 
removeOnGesturingListener(OnGesturingListener listener)370     public void removeOnGesturingListener(OnGesturingListener listener) {
371         mOnGesturingListeners.remove(listener);
372     }
373 
removeAllOnGesturingListeners()374     public void removeAllOnGesturingListeners() {
375         mOnGesturingListeners.clear();
376     }
377 
isGesturing()378     public boolean isGesturing() {
379         return mIsGesturing;
380     }
381 
setCurrentColor(int color)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      */
getGesturePaint()395     public Paint getGesturePaint() {
396         return mGesturePaint;
397     }
398 
399     @Override
draw(Canvas canvas)400     public void draw(Canvas canvas) {
401         super.draw(canvas);
402 
403         if (mCurrentGesture != null && mGestureVisible) {
404             canvas.drawPath(mPath, mGesturePaint);
405         }
406     }
407 
setPaintAlpha(int alpha)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 
clear(boolean animated)415     public void clear(boolean animated) {
416         clear(animated, false, true);
417     }
418 
clear(boolean animated, boolean fireActionPerformed, boolean immediate)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 
cancelClearAnimation()455     public void cancelClearAnimation() {
456         setPaintAlpha(255);
457         mIsFadingOut = false;
458         mFadingHasStarted = false;
459         removeCallbacks(mFadingOut);
460         mPath.rewind();
461         mCurrentGesture = null;
462     }
463 
cancelGesture()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
onDetachedFromWindow()496     protected void onDetachedFromWindow() {
497         super.onDetachedFromWindow();
498         cancelClearAnimation();
499     }
500 
501     @Override
dispatchTouchEvent(MotionEvent event)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 
processEvent(MotionEvent event)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 
touchDown(MotionEvent event)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 
touchMove(MotionEvent event)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 
touchUp(MotionEvent event, boolean cancel)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 
cancelGesture(MotionEvent event)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 
fireOnGesturePerformed()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 
run()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 {
onGesturingStarted(GestureOverlayView overlay)786         void onGesturingStarted(GestureOverlayView overlay);
787 
onGesturingEnded(GestureOverlayView overlay)788         void onGesturingEnded(GestureOverlayView overlay);
789     }
790 
791     public static interface OnGestureListener {
onGestureStarted(GestureOverlayView overlay, MotionEvent event)792         void onGestureStarted(GestureOverlayView overlay, MotionEvent event);
793 
onGesture(GestureOverlayView overlay, MotionEvent event)794         void onGesture(GestureOverlayView overlay, MotionEvent event);
795 
onGestureEnded(GestureOverlayView overlay, MotionEvent event)796         void onGestureEnded(GestureOverlayView overlay, MotionEvent event);
797 
onGestureCancelled(GestureOverlayView overlay, MotionEvent event)798         void onGestureCancelled(GestureOverlayView overlay, MotionEvent event);
799     }
800 
801     public static interface OnGesturePerformedListener {
onGesturePerformed(GestureOverlayView overlay, Gesture gesture)802         void onGesturePerformed(GestureOverlayView overlay, Gesture gesture);
803     }
804 }
805