blob: e0d454cb44d05c73eb85d0c9ffdc1a0caaf35d82 [file] [log] [blame]
/*
* 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);
}
}