blob: 99078319b26ffb48eb75beb820a3bfe9d4d7a710 [file] [log] [blame]
/*
* Copyright (C) 2008-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 com.android.gesture;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
/**
* A (transparent) overlay for gesture input that can be placed on top of other
* widgets. The view can also be opaque.
*/
public class GestureOverlay extends View {
static final float TOUCH_TOLERANCE = 3;
private static final int TRANSPARENT_BACKGROUND = Color.argb(0, 0, 0, 0);
private static final float FADING_ALPHA_CHANGE = 0.03f;
private static final long FADING_REFRESHING_RATE = 100;
private static final int GESTURE_STROKE_WIDTH = 12;
private static final boolean GESTURE_RENDERING_ANTIALIAS = true;
private static final BlurMaskFilter BLUR_MASK_FILTER = new BlurMaskFilter(1, BlurMaskFilter.Blur.NORMAL);
private static final boolean DITHER_FLAG = true;
private static final int REFRESH_RANGE = 10;
public static final int DEFAULT_GESTURE_COLOR = Color.argb(255, 255, 255, 0);
// double buffering
private Paint mGesturePaint;
private Bitmap mBitmap; // with transparent background
private Canvas mBitmapCanvas;
// for rendering immediate ink feedback
private Rect mInvalidRect = new Rect();
private Path mPath;
private float mX;
private float mY;
private float mCurveEndX;
private float mCurveEndY;
// current gesture
private Gesture mCurrentGesture = null;
// gesture event handlers
ArrayList<GestureListener> mGestureListeners = new ArrayList<GestureListener>();
private ArrayList<GesturePoint> mPointBuffer = null;
// fading out effect
private boolean mIsFadingOut = false;
private float mFadingAlpha = 1;
private Handler mHandler = new Handler();
private Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG);
private Runnable mFadingOut = new Runnable() {
public void run() {
if (mIsFadingOut) {
mFadingAlpha -= FADING_ALPHA_CHANGE;
if (mFadingAlpha <= 0) {
mIsFadingOut = false;
mPath = null;
mCurrentGesture = null;
mBitmap.eraseColor(TRANSPARENT_BACKGROUND);
} else {
mHandler.postDelayed(this, FADING_REFRESHING_RATE);
}
invalidate();
}
}
};
public GestureOverlay(Context context) {
super(context);
init();
}
public GestureOverlay(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ArrayList<GesturePoint> getCurrentStroke() {
return mPointBuffer;
}
public Gesture getCurrentGesture() {
return mCurrentGesture;
}
/**
* Set Gesture color
*
* @param color
*/
public void setGestureColor(int color) {
mGesturePaint.setColor(color);
if (mCurrentGesture != null) {
mBitmap.eraseColor(TRANSPARENT_BACKGROUND);
mCurrentGesture.draw(mBitmapCanvas, mGesturePaint);
}
}
/**
* Set the gesture to be shown in the view
*
* @param gesture
*/
public void setCurrentGesture(Gesture gesture) {
if (mCurrentGesture != null) {
clear(false);
}
mCurrentGesture = gesture;
if (gesture != null) {
if (mBitmapCanvas != null) {
gesture.draw(mBitmapCanvas, mGesturePaint);
invalidate();
}
}
}
private void init() {
mGesturePaint = new Paint();
mGesturePaint.setAntiAlias(GESTURE_RENDERING_ANTIALIAS);
mGesturePaint.setColor(DEFAULT_GESTURE_COLOR);
mGesturePaint.setStyle(Paint.Style.STROKE);
mGesturePaint.setStrokeJoin(Paint.Join.ROUND);
mGesturePaint.setStrokeCap(Paint.Cap.ROUND);
mGesturePaint.setStrokeWidth(GESTURE_STROKE_WIDTH);
mGesturePaint.setDither(DITHER_FLAG);
mPath = null;
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
if (width <= 0 || height <= 0) {
return;
}
int targetWidth = width > oldWidth ? width : oldWidth;
int targetHeight = height > oldHeight ? height : oldHeight;
mBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
mBitmapCanvas = new Canvas(mBitmap);
mBitmapCanvas.drawColor(TRANSPARENT_BACKGROUND);
if (mCurrentGesture != null) {
mCurrentGesture.draw(mBitmapCanvas, mGesturePaint);
}
}
public void addGestureListener(GestureListener listener) {
mGestureListeners.add(listener);
}
public void removeGestureListener(GestureListener listener) {
mGestureListeners.remove(listener);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw double buffer
if (mIsFadingOut) {
mBitmapPaint.setAlpha((int) (255 * mFadingAlpha));
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
} else {
mBitmapPaint.setAlpha(255);
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
}
// draw the current stroke
if (mPath != null) {
canvas.drawPath(mPath, mGesturePaint);
}
}
/**
* Clear up the overlay
*
* @param fadeOut whether the gesture on the overlay should fade out
* gradually or disappear immediately
*/
public void clear(boolean fadeOut) {
if (fadeOut) {
mFadingAlpha = 1;
mIsFadingOut = true;
mHandler.removeCallbacks(mFadingOut);
mHandler.postDelayed(mFadingOut, FADING_REFRESHING_RATE);
} else {
mPath = null;
mCurrentGesture = null;
if (mBitmap != null) {
mBitmap.eraseColor(TRANSPARENT_BACKGROUND);
invalidate();
}
}
}
public void cancelFadingOut() {
mIsFadingOut = false;
mHandler.removeCallbacks(mFadingOut);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Rect rect = touchStart(event);
invalidate(rect);
break;
case MotionEvent.ACTION_MOVE:
rect = touchMove(event);
if (rect != null) {
invalidate(rect);
}
break;
case MotionEvent.ACTION_UP:
touchUp(event);
invalidate();
break;
}
return true;
}
private Rect touchStart(MotionEvent event) {
// pass the event to handlers
ArrayList<GestureListener> listeners = mGestureListeners;
int count = listeners.size();
for (int i = 0; i < count; i++) {
GestureListener listener = listeners.get(i);
listener.onStartGesture(this, event);
}
// if there is fading out going on, stop it.
if (mIsFadingOut) {
mIsFadingOut = false;
mHandler.removeCallbacks(mFadingOut);
mBitmap.eraseColor(TRANSPARENT_BACKGROUND);
mCurrentGesture = null;
}
float x = event.getX();
float y = event.getY();
mX = x;
mY = y;
if (mCurrentGesture == null) {
mCurrentGesture = new Gesture();
}
mPointBuffer = new ArrayList<GesturePoint>();
mPointBuffer.add(new GesturePoint(x, y, event.getEventTime()));
mPath = new Path();
mPath.moveTo(x, y);
mInvalidRect.set((int) x - REFRESH_RANGE, (int) y - REFRESH_RANGE, (int) x + REFRESH_RANGE,
(int) y + REFRESH_RANGE);
mCurveEndX = x;
mCurveEndY = y;
return mInvalidRect;
}
private Rect touchMove(MotionEvent event) {
Rect areaToRefresh = null;
float x = event.getX();
float y = event.getY();
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
// start with the curve end
mInvalidRect.set((int) mCurveEndX - REFRESH_RANGE, (int) mCurveEndY - REFRESH_RANGE,
(int) mCurveEndX + REFRESH_RANGE, (int) mCurveEndY + REFRESH_RANGE);
mCurveEndX = (x + mX) / 2;
mCurveEndY = (y + mY) / 2;
mPath.quadTo(mX, mY, mCurveEndX, mCurveEndY);
// union with the control point of the new curve
mInvalidRect.union((int) mX - REFRESH_RANGE, (int) mY - REFRESH_RANGE,
(int) mX + REFRESH_RANGE, (int) mY + REFRESH_RANGE);
// union with the end point of the new curve
mInvalidRect.union((int) mCurveEndX - REFRESH_RANGE, (int) mCurveEndY - REFRESH_RANGE,
(int) mCurveEndX + REFRESH_RANGE, (int) mCurveEndY + REFRESH_RANGE);
areaToRefresh = mInvalidRect;
mX = x;
mY = y;
}
mPointBuffer.add(new GesturePoint(x, y, event.getEventTime()));
// pass the event to handlers
ArrayList<GestureListener> listeners = mGestureListeners;
int count = listeners.size();
for (int i = 0; i < count; i++) {
GestureListener listener = listeners.get(i);
listener.onGesture(this, event);
}
return areaToRefresh;
}
private void touchUp(MotionEvent event) {
// add the stroke to the current gesture
mCurrentGesture.addStroke(new GestureStroke(mPointBuffer));
// add the stroke to the double buffer
mGesturePaint.setMaskFilter(BLUR_MASK_FILTER);
mBitmapCanvas.drawPath(mPath, mGesturePaint);
mGesturePaint.setMaskFilter(null);
// pass the event to handlers
ArrayList<GestureListener> listeners = mGestureListeners;
int count = listeners.size();
for (int i = 0; i < count; i++) {
GestureListener listener = listeners.get(i);
listener.onFinishGesture(this, event);
}
mPath = null;
mPointBuffer = null;
}
}