content: /*
   * Copyright (C) 2009 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.gesture;
  
  import android.annotation.ColorInt;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Canvas;
  import android.graphics.Paint;
  import android.graphics.Path;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.util.AttributeSet;
  import android.view.MotionEvent;
  import android.view.animation.AnimationUtils;
  import android.view.animation.AccelerateDecelerateInterpolator;
  import android.widget.FrameLayout;
  import android.os.SystemClock;
  import android.annotation.Widget;
  import com.android.internal.R;
  
  import java.util.ArrayList;
  
  /**
   * A transparent overlay for gesture input that can be placed on top of other
   * widgets or contain other widgets.
   *
   * @attr ref android.R.styleable#GestureOverlayView_eventsInterceptionEnabled
   * @attr ref android.R.styleable#GestureOverlayView_fadeDuration
   * @attr ref android.R.styleable#GestureOverlayView_fadeOffset
   * @attr ref android.R.styleable#GestureOverlayView_fadeEnabled
   * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeWidth
   * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeAngleThreshold
   * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeLengthThreshold
   * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeSquarenessThreshold
   * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeType
   * @attr ref android.R.styleable#GestureOverlayView_gestureColor
   * @attr ref android.R.styleable#GestureOverlayView_orientation
   * @attr ref android.R.styleable#GestureOverlayView_uncertainGestureColor
   */
  @Widget
  public class GestureOverlayView extends FrameLayout {
      public static final int GESTURE_STROKE_TYPE_SINGLE = 0;
      public static final int GESTURE_STROKE_TYPE_MULTIPLE = 1;
  
      public static final int ORIENTATION_HORIZONTAL = 0;
      public static final int ORIENTATION_VERTICAL = 1;
  
      private static final int FADE_ANIMATION_RATE = 16;
      private static final boolean GESTURE_RENDERING_ANTIALIAS = true;
      private static final boolean DITHER_FLAG = true;
  
      private final Paint mGesturePaint = new Paint();
  
      private long mFadeDuration = 150;
      private long mFadeOffset = 420;
      private long mFadingStart;
      private boolean mFadingHasStarted;
      private boolean mFadeEnabled = true;
  
      private int mCurrentColor;
      private int mCertainGestureColor = 0xFFFFFF00;
      private int mUncertainGestureColor = 0x48FFFF00;
      private float mGestureStrokeWidth = 12.0f;
      private int mInvalidateExtraBorder = 10;
  
      private int mGestureStrokeType = GESTURE_STROKE_TYPE_SINGLE;
      private float mGestureStrokeLengthThreshold = 50.0f;
      private float mGestureStrokeSquarenessTreshold = 0.275f;
      private float mGestureStrokeAngleThreshold = 40.0f;
  
      private int mOrientation = ORIENTATION_VERTICAL;
  
      private final Rect mInvalidRect = new Rect();
      private final Path mPath = new Path();
      private boolean mGestureVisible = true;
  
      private float mX;
      private float mY;
  
      private float mCurveEndX;
      private float mCurveEndY;
  
      private float mTotalLength;
      private boolean mIsGesturing = false;
      private boolean mPreviousWasGesturing = false;
      private boolean mInterceptEvents = true;
      private boolean mIsListeningForGestures;
      private boolean mResetGesture;
  
      // current gesture
      private Gesture mCurrentGesture;
      private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100);
  
      // TODO: Make this a list of WeakReferences
      private final ArrayList<OnGestureListener> mOnGestureListeners =
              new ArrayList<OnGestureListener>();
      // TODO: Make this a list of WeakReferences
      private final ArrayList<OnGesturePerformedListener> mOnGesturePerformedListeners =
              new ArrayList<OnGesturePerformedListener>();
      // TODO: Make this a list of WeakReferences
      private final ArrayList<OnGesturingListener> mOnGesturingListeners =
              new ArrayList<OnGesturingListener>();
  
      private boolean mHandleGestureActions;
  
      // fading out effect
      private boolean mIsFadingOut = false;
      private float mFadingAlpha = 1.0f;
      private final AccelerateDecelerateInterpolator mInterpolator =
              new AccelerateDecelerateInterpolator();
  
      private final FadeOutRunnable mFadingOut = new FadeOutRunnable();
  
      public GestureOverlayView(Context context) {
          super(context);
          init();
      }
  
      public GestureOverlayView(Context context, AttributeSet attrs) {
          this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle);
      }
  
      public GestureOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public GestureOverlayView(
              Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
  
          final TypedArray a = context.obtainStyledAttributes(
                  attrs, R.styleable.GestureOverlayView, defStyleAttr, defStyleRes);
  
          mGestureStrokeWidth = a.getFloat(R.styleable.GestureOverlayView_gestureStrokeWidth,
                  mGestureStrokeWidth);
          mInvalidateExtraBorder = Math.max(1, ((int) mGestureStrokeWidth) - 1);
          mCertainGestureColor = a.getColor(R.styleable.GestureOverlayView_gestureColor,
                  mCertainGestureColor);
          mUncertainGestureColor = a.getColor(R.styleable.GestureOverlayView_uncertainGestureColor,
                  mUncertainGestureColor);
          mFadeDuration = a.getInt(R.styleable.GestureOverlayView_fadeDuration, (int) mFadeDuration);
          mFadeOffset = a.getInt(R.styleable.GestureOverlayView_fadeOffset, (int) mFadeOffset);
          mGestureStrokeType = a.getInt(R.styleable.GestureOverlayView_gestureStrokeType,
                  mGestureStrokeType);
          mGestureStrokeLengthThreshold = a.getFloat(
                  R.styleable.GestureOverlayView_gestureStrokeLengthThreshold,
                  mGestureStrokeLengthThreshold);
          mGestureStrokeAngleThreshold = a.getFloat(
                  R.styleable.GestureOverlayView_gestureStrokeAngleThreshold,
                  mGestureStrokeAngleThreshold);
          mGestureStrokeSquarenessTreshold = a.getFloat(
                  R.styleable.GestureOverlayView_gestureStrokeSquarenessThreshold,
                  mGestureStrokeSquarenessTreshold);
          mInterceptEvents = a.getBoolean(R.styleable.GestureOverlayView_eventsInterceptionEnabled,
                  mInterceptEvents);
          mFadeEnabled = a.getBoolean(R.styleable.GestureOverlayView_fadeEnabled,
                  mFadeEnabled);
          mOrientation = a.getInt(R.styleable.GestureOverlayView_orientation, mOrientation);
  
          a.recycle();
  
          init();
      }
  
      private void init() {
          setWillNotDraw(false);
  
          final Paint gesturePaint = mGesturePaint;
          gesturePaint.setAntiAlias(GESTURE_RENDERING_ANTIALIAS);
          gesturePaint.setColor(mCertainGestureColor);
          gesturePaint.setStyle(Paint.Style.STROKE);
          gesturePaint.setStrokeJoin(Paint.Join.ROUND);
          gesturePaint.setStrokeCap(Paint.Cap.ROUND);
          gesturePaint.setStrokeWidth(mGestureStrokeWidth);
          gesturePaint.setDither(DITHER_FLAG);
  
          mCurrentColor = mCertainGestureColor;
          setPaintAlpha(255);
      }
  
      public ArrayList<GesturePoint> getCurrentStroke() {
          return mStrokeBuffer;
      }
  
      public int getOrientation() {
          return mOrientation;
      }
  
      public void setOrientation(int orientation) {
          mOrientation = orientation;
      }
  
      public void setGestureColor(@ColorInt int color) {
          mCertainGestureColor = color;
      }
  
      public void setUncertainGestureColor(@ColorInt int color) {
          mUncertainGestureColor = color;
      }
  
      @ColorInt
      public int getUncertainGestureColor() {
          return mUncertainGestureColor;
      }
  
      @ColorInt
      public int getGestureColor() {
          return mCertainGestureColor;
      }
  
      public float getGestureStrokeWidth() {
          return mGestureStrokeWidth;
      }
  
      public void setGestureStrokeWidth(float gestureStrokeWidth) {
          mGestureStrokeWidth = gestureStrokeWidth;
          mInvalidateExtraBorder = Math.max(1, ((int) gestureStrokeWidth) - 1);
          mGesturePaint.setStrokeWidth(gestureStrokeWidth);
      }
  
      public int getGestureStrokeType() {
          return mGestureStrokeType;
      }
  
      public void setGestureStrokeType(int gestureStrokeType) {
          mGestureStrokeType = gestureStrokeType;
      }
  
      public float getGestureStrokeLengthThreshold() {
          return mGestureStrokeLengthThreshold;
      }
  
      public void setGestureStrokeLengthThreshold(float gestureStrokeLengthThreshold) {
          mGestureStrokeLengthThreshold = gestureStrokeLengthThreshold;
      }
  
      public float getGestureStrokeSquarenessTreshold() {
          return mGestureStrokeSquarenessTreshold;
      }
  
      public void setGestureStrokeSquarenessTreshold(float gestureStrokeSquarenessTreshold) {
          mGestureStrokeSquarenessTreshold = gestureStrokeSquarenessTreshold;
      }
  
      public float getGestureStrokeAngleThreshold() {
          return mGestureStrokeAngleThreshold;
      }
  
      public void setGestureStrokeAngleThreshold(float gestureStrokeAngleThreshold) {
          mGestureStrokeAngleThreshold = gestureStrokeAngleThreshold;
      }
  
      public boolean isEventsInterceptionEnabled() {
          return mInterceptEvents;
      }
  
      public void setEventsInterceptionEnabled(boolean enabled) {
          mInterceptEvents = enabled;
      }
  
      public boolean isFadeEnabled() {
          return mFadeEnabled;
      }
  
      public void setFadeEnabled(boolean fadeEnabled) {
          mFadeEnabled = fadeEnabled;
      }
  
      public Gesture getGesture() {
          return mCurrentGesture;
      }
  
      public void setGesture(Gesture gesture) {
          if (mCurrentGesture != null) {
              clear(false);
          }
  
          setCurrentColor(mCertainGestureColor);
          mCurrentGesture = gesture;
  
          final Path path = mCurrentGesture.toPath();
          final RectF bounds = new RectF();
          path.computeBounds(bounds, true);
  
          // TODO: The path should also be scaled to fit inside this view
          mPath.rewind();
          mPath.addPath(path, -bounds.left + (getWidth() - bounds.width()) / 2.0f,
                  -bounds.top + (getHeight() - bounds.height()) / 2.0f);
  
          mResetGesture = true;
  
          invalidate();
      }
  
      public Path getGesturePath() {
          return mPath;
      }
  
      public Path getGesturePath(Path path) {
          path.set(mPath);
          return path;
      }
  
      public boolean isGestureVisible() {
          return mGestureVisible;
      }
  
      public void setGestureVisible(boolean visible) {
          mGestureVisible = visible;
      }
  
      public long getFadeOffset() {
          return mFadeOffset;
      }
  
      public void setFadeOffset(long fadeOffset) {
          mFadeOffset = fadeOffset;
      }
  
      public void addOnGestureListener(OnGestureListener listener) {
          mOnGestureListeners.add(listener);
      }
  
      public void removeOnGestureListener(OnGestureListener listener) {
          mOnGestureListeners.remove(listener);
      }
  
      public void removeAllOnGestureListeners() {
          mOnGestureListeners.clear();
      }
  
      public void addOnGesturePerformedListener(OnGesturePerformedListener listener) {
          mOnGesturePerformedListeners.add(listener);
          if (mOnGesturePerformedListeners.size() > 0) {
              mHandleGestureActions = true;
          }
      }
  
      public void removeOnGesturePerformedListener(OnGesturePerformedListener listener) {
          mOnGesturePerformedListeners.remove(listener);
          if (mOnGesturePerformedListeners.size() <= 0) {
              mHandleGestureActions = false;
          }
      }
  
      public void removeAllOnGesturePerformedListeners() {
          mOnGesturePerformedListeners.clear();
          mHandleGestureActions = false;
      }
  
      public void addOnGesturingListener(OnGesturingListener listener) {
          mOnGesturingListeners.add(listener);
      }
  
      public void removeOnGesturingListener(OnGesturingListener listener) {
          mOnGesturingListeners.remove(listener);
      }
  
      public void removeAllOnGesturingListeners() {
          mOnGesturingListeners.clear();
      }
  
      public boolean isGesturing() {
          return mIsGesturing;
      }
  
      private void setCurrentColor(int color) {
          mCurrentColor = color;
          if (mFadingHasStarted) {
              setPaintAlpha((int) (255 * mFadingAlpha));
          } else {
              setPaintAlpha(255);
          }
          invalidate();
      }
  
      /**
       * @hide
       */
      public Paint getGesturePaint() {
          return mGesturePaint;
      }
  
      @Override
      public void draw(Canvas canvas) {
          super.draw(canvas);
  
          if (mCurrentGesture != null && mGestureVisible) {
              canvas.drawPath(mPath, mGesturePaint);
          }
      }
  
      private void setPaintAlpha(int alpha) {
          alpha += alpha >> 7;
          final int baseAlpha = mCurrentColor >>> 24;
          final int useAlpha = baseAlpha * alpha >> 8;
          mGesturePaint.setColor((mCurrentColor << 8 >>> 8) | (useAlpha << 24));
      }
  
      public void clear(boolean animated) {
          clear(animated, false, true);
      }
  
      private void clear(boolean animated, boolean fireActionPerformed, boolean immediate) {
          setPaintAlpha(255);
          removeCallbacks(mFadingOut);
          mResetGesture = false;
          mFadingOut.fireActionPerformed = fireActionPerformed;
          mFadingOut.resetMultipleStrokes = false;
  
          if (animated && mCurrentGesture != null) {
              mFadingAlpha = 1.0f;
              mIsFadingOut = true;
              mFadingHasStarted = false;
              mFadingStart = AnimationUtils.currentAnimationTimeMillis() + mFadeOffset;
  
              postDelayed(mFadingOut, mFadeOffset);
          } else {
              mFadingAlpha = 1.0f;
              mIsFadingOut = false;
              mFadingHasStarted = false;
  
              if (immediate) {
                  mCurrentGesture = null;
                  mPath.rewind();
                  invalidate();
              } else if (fireActionPerformed) {
                  postDelayed(mFadingOut, mFadeOffset);
              } else if (mGestureStrokeType == GESTURE_STROKE_TYPE_MULTIPLE) {
                  mFadingOut.resetMultipleStrokes = true;
                  postDelayed(mFadingOut, mFadeOffset);
              } else {
                  mCurrentGesture = null;
                  mPath.rewind();
                  invalidate();
              }
          }
      }
  
      public void cancelClearAnimation() {
          setPaintAlpha(255);
          mIsFadingOut = false;
          mFadingHasStarted = false;
          removeCallbacks(mFadingOut);
          mPath.rewind();
          mCurrentGesture = null;
      }
  
      public void cancelGesture() {
          mIsListeningForGestures = false;
  
          // add the stroke to the current gesture
          mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
  
          // pass the event to handlers
          final long now = SystemClock.uptimeMillis();
          final MotionEvent event = MotionEvent.obtain(now, now,
                  MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
  
          final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
          int count = listeners.size();
          for (int i = 0; i < count; i++) {
              listeners.get(i).onGestureCancelled(this, event);
          }
  
          event.recycle();
  
          clear(false);
          mIsGesturing = false;
          mPreviousWasGesturing = false;
          mStrokeBuffer.clear();
  
          final ArrayList<OnGesturingListener> otherListeners = mOnGesturingListeners;
          count = otherListeners.size();
          for (int i = 0; i < count; i++) {
              otherListeners.get(i).onGesturingEnded(this);
          }
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          cancelClearAnimation();
      }
  
      @Override
      public boolean dispatchTouchEvent(MotionEvent event) {
          if (isEnabled()) {
              final boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null &&
                      mCurrentGesture.getStrokesCount() > 0 && mPreviousWasGesturing)) &&
                      mInterceptEvents;
  
              processEvent(event);
  
              if (cancelDispatch) {
                  event.setAction(MotionEvent.ACTION_CANCEL);
              }
  
              super.dispatchTouchEvent(event);
  
              return true;
          }
  
          return super.dispatchTouchEvent(event);
      }
  
      private boolean processEvent(MotionEvent event) {
          switch (event.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  touchDown(event);
                  invalidate();
                  return true;
              case MotionEvent.ACTION_MOVE:
                  if (mIsListeningForGestures) {
                      Rect rect = touchMove(event);
                      if (rect != null) {
                          invalidate(rect);
                      }
                      return true;
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  if (mIsListeningForGestures) {
                      touchUp(event, false);
                      invalidate();
                      return true;
                  }
                  break;
              case MotionEvent.ACTION_CANCEL:
                  if (mIsListeningForGestures) {
                      touchUp(event, true);
                      invalidate();
                      return true;
                  }
          }
  
          return false;
      }
  
      private void touchDown(MotionEvent event) {
          mIsListeningForGestures = true;
  
          float x = event.getX();
          float y = event.getY();
  
          mX = x;
          mY = y;
  
          mTotalLength = 0;
          mIsGesturing = false;
  
          if (mGestureStrokeType == GESTURE_STROKE_TYPE_SINGLE || mResetGesture) {
              if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
              mResetGesture = false;
              mCurrentGesture = null;
              mPath.rewind();
          } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) {
              if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor);
          }
  
          // if there is fading out going on, stop it.
          if (mFadingHasStarted) {
              cancelClearAnimation();
          } else if (mIsFadingOut) {
              setPaintAlpha(255);
              mIsFadingOut = false;
              mFadingHasStarted = false;
              removeCallbacks(mFadingOut);
          }
  
          if (mCurrentGesture == null) {
              mCurrentGesture = new Gesture();
          }
  
          mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
          mPath.moveTo(x, y);
  
          final int border = mInvalidateExtraBorder;
          mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border);
  
          mCurveEndX = x;
          mCurveEndY = y;
  
          // pass the event to handlers
          final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
          final int count = listeners.size();
          for (int i = 0; i < count; i++) {
              listeners.get(i).onGestureStarted(this, event);
          }
      }
  
      private Rect touchMove(MotionEvent event) {
          Rect areaToRefresh = null;
  
          final float x = event.getX();
          final float y = event.getY();
  
          final float previousX = mX;
          final float previousY = mY;
  
          final float dx = Math.abs(x - previousX);
          final float dy = Math.abs(y - previousY);
  
          if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) {
              areaToRefresh = mInvalidRect;
  
              // start with the curve end
              final int border = mInvalidateExtraBorder;
              areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border,
                      (int) mCurveEndX + border, (int) mCurveEndY + border);
  
              float cX = mCurveEndX = (x + previousX) / 2;
              float cY = mCurveEndY = (y + previousY) / 2;
  
              mPath.quadTo(previousX, previousY, cX, cY);
  
              // union with the control point of the new curve
              areaToRefresh.union((int) previousX - border, (int) previousY - border,
                      (int) previousX + border, (int) previousY + border);
  
              // union with the end point of the new curve
              areaToRefresh.union((int) cX - border, (int) cY - border,
                      (int) cX + border, (int) cY + border);
  
              mX = x;
              mY = y;
  
              mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime()));
  
              if (mHandleGestureActions && !mIsGesturing) {
                  mTotalLength += (float) Math.hypot(dx, dy);
  
                  if (mTotalLength > mGestureStrokeLengthThreshold) {
                      final OrientedBoundingBox box =
                              GestureUtils.computeOrientedBoundingBox(mStrokeBuffer);
  
                      float angle = Math.abs(box.orientation);
                      if (angle > 90) {
                          angle = 180 - angle;
                      }
  
                      if (box.squareness > mGestureStrokeSquarenessTreshold ||
                              (mOrientation == ORIENTATION_VERTICAL ?
                                      angle < mGestureStrokeAngleThreshold :
                                      angle > mGestureStrokeAngleThreshold)) {
  
                          mIsGesturing = true;
                          setCurrentColor(mCertainGestureColor);
  
                          final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
                          int count = listeners.size();
                          for (int i = 0; i < count; i++) {
                              listeners.get(i).onGesturingStarted(this);
                          }
                      }
                  }
              }
  
              // pass the event to handlers
              final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
              final int count = listeners.size();
              for (int i = 0; i < count; i++) {
                  listeners.get(i).onGesture(this, event);
              }
          }
  
          return areaToRefresh;
      }
  
      private void touchUp(MotionEvent event, boolean cancel) {
          mIsListeningForGestures = false;
  
          // A gesture wasn't started or was cancelled
          if (mCurrentGesture != null) {
              // add the stroke to the current gesture
              mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer));
  
              if (!cancel) {
                  // pass the event to handlers
                  final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
                  int count = listeners.size();
                  for (int i = 0; i < count; i++) {
                      listeners.get(i).onGestureEnded(this, event);
                  }
  
                  clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing,
                          false);
              } else {
                  cancelGesture(event);
  
              }
          } else {
              cancelGesture(event);
          }
  
          mStrokeBuffer.clear();
          mPreviousWasGesturing = mIsGesturing;
          mIsGesturing = false;
  
          final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners;
          int count = listeners.size();
          for (int i = 0; i < count; i++) {
              listeners.get(i).onGesturingEnded(this);
          }
      }
  
      private void cancelGesture(MotionEvent event) {
          // pass the event to handlers
          final ArrayList<OnGestureListener> listeners = mOnGestureListeners;
          final int count = listeners.size();
          for (int i = 0; i < count; i++) {
              listeners.get(i).onGestureCancelled(this, event);
          }
  
          clear(false);
      }
  
      private void fireOnGesturePerformed() {
          final ArrayList<OnGesturePerformedListener> actionListeners = mOnGesturePerformedListeners;
          final int count = actionListeners.size();
          for (int i = 0; i < count; i++) {
              actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, mCurrentGesture);
          }
      }
  
      private class FadeOutRunnable implements Runnable {
          boolean fireActionPerformed;
          boolean resetMultipleStrokes;
  
          public void run() {
              if (mIsFadingOut) {
                  final long now = AnimationUtils.currentAnimationTimeMillis();
                  final long duration = now - mFadingStart;
  
                  if (duration > mFadeDuration) {
                      if (fireActionPerformed) {
                          fireOnGesturePerformed();
                      }
  
                      mPreviousWasGesturing = false;
                      mIsFadingOut = false;
                      mFadingHasStarted = false;
                      mPath.rewind();
                      mCurrentGesture = null;
                      setPaintAlpha(255);
                  } else {
                      mFadingHasStarted = true;
                      float interpolatedTime = Math.max(0.0f,
                              Math.min(1.0f, duration / (float) mFadeDuration));
                      mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime);
                      setPaintAlpha((int) (255 * mFadingAlpha));
                      postDelayed(this, FADE_ANIMATION_RATE);
                  }
              } else if (resetMultipleStrokes) {
                  mResetGesture = true;
              } else {
                  fireOnGesturePerformed();
  
                  mFadingHasStarted = false;
                  mPath.rewind();
                  mCurrentGesture = null;
                  mPreviousWasGesturing = false;
                  setPaintAlpha(255);
              }
  
              invalidate();
          }
      }
  
      public static interface OnGesturingListener {
          void onGesturingStarted(GestureOverlayView overlay);
  
          void onGesturingEnded(GestureOverlayView overlay);
      }
  
      public static interface OnGestureListener {
          void onGestureStarted(GestureOverlayView overlay, MotionEvent event);
  
          void onGesture(GestureOverlayView overlay, MotionEvent event);
  
          void onGestureEnded(GestureOverlayView overlay, MotionEvent event);
  
          void onGestureCancelled(GestureOverlayView overlay, MotionEvent event);
      }
  
      public static interface OnGesturePerformedListener {
          void onGesturePerformed(GestureOverlayView overlay, Gesture gesture);
      }
  }
  