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