diff options
Diffstat (limited to 'core/java/android')
-rwxr-xr-x | core/java/android/gesture/Gesture.java | 298 | ||||
-rw-r--r-- | core/java/android/gesture/GestureConstants.java | 26 | ||||
-rw-r--r-- | core/java/android/gesture/GestureLibrary.java | 346 | ||||
-rwxr-xr-x | core/java/android/gesture/GestureOverlayView.java | 419 | ||||
-rw-r--r-- | core/java/android/gesture/GesturePoint.java | 46 | ||||
-rw-r--r-- | core/java/android/gesture/GestureStroke.java | 215 | ||||
-rwxr-xr-x | core/java/android/gesture/GestureUtilities.java | 444 | ||||
-rwxr-xr-x | core/java/android/gesture/Instance.java | 115 | ||||
-rw-r--r-- | core/java/android/gesture/InstanceLearner.java | 91 | ||||
-rwxr-xr-x | core/java/android/gesture/Learner.java | 83 | ||||
-rw-r--r-- | core/java/android/gesture/LetterRecognizer.java | 273 | ||||
-rw-r--r-- | core/java/android/gesture/OrientedBoundingBox.java | 85 | ||||
-rwxr-xr-x | core/java/android/gesture/Prediction.java | 33 | ||||
-rw-r--r-- | core/java/android/gesture/TouchThroughGestureListener.java | 171 |
14 files changed, 2645 insertions, 0 deletions
diff --git a/core/java/android/gesture/Gesture.java b/core/java/android/gesture/Gesture.java new file mode 100755 index 0000000..14530a1 --- /dev/null +++ b/core/java/android/gesture/Gesture.java @@ -0,0 +1,298 @@ +/* + * 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 android.gesture; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.io.IOException; +import java.io.DataOutputStream; +import java.io.DataInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; + +/** + * A gesture can have a single or multiple strokes + */ + +public class Gesture implements Parcelable { + private static final long GESTURE_ID_BASE = System.currentTimeMillis(); + + private static final int BITMAP_RENDERING_WIDTH = 2; + + private static final boolean BITMAP_RENDERING_ANTIALIAS = true; + private static final boolean BITMAP_RENDERING_DITHER = true; + + private static int sGestureCount = 0; + + private RectF mBoundingBox; + + // the same as its instance ID + private long mGestureID; + + private final ArrayList<GestureStroke> mStrokes = new ArrayList<GestureStroke>(); + + public Gesture() { + mGestureID = GESTURE_ID_BASE + sGestureCount++; + } + + /** + * @return all the strokes of the gesture + */ + public ArrayList<GestureStroke> getStrokes() { + return mStrokes; + } + + /** + * @return the number of strokes included by this gesture + */ + public int getStrokesCount() { + return mStrokes.size(); + } + + /** + * Add a stroke to the gesture + * + * @param stroke + */ + public void addStroke(GestureStroke stroke) { + mStrokes.add(stroke); + + if (mBoundingBox == null) { + mBoundingBox = new RectF(stroke.boundingBox); + } else { + mBoundingBox.union(stroke.boundingBox); + } + } + + /** + * Get the total length of the gesture. When there are multiple strokes in + * the gesture, this returns the sum of the lengths of all the strokes + * + * @return the length of the gesture + */ + public float getLength() { + int len = 0; + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + len += strokes.get(i).length; + } + + return len; + } + + /** + * @return the bounding box of the gesture + */ + public RectF getBoundingBox() { + return mBoundingBox; + } + + /** + * Set the id of the gesture + * + * @param id + */ + void setID(long id) { + mGestureID = id; + } + + /** + * @return the id of the gesture + */ + public long getID() { + return mGestureID; + } + + /** + * draw the gesture + * + * @param canvas + */ + void draw(Canvas canvas, Paint paint) { + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + strokes.get(i).draw(canvas, paint); + } + } + + /** + * Create a bitmap of the gesture with a transparent background + * + * @param width width of the target bitmap + * @param height height of the target bitmap + * @param edge the edge + * @param numSample + * @param color + * @return the bitmap + */ + public Bitmap toBitmap(int width, int height, int edge, int numSample, int color) { + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + canvas.translate(edge, edge); + + final Paint paint = new Paint(); + paint.setAntiAlias(BITMAP_RENDERING_ANTIALIAS); + paint.setDither(BITMAP_RENDERING_DITHER); + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(BITMAP_RENDERING_WIDTH); + + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + Path path = strokes.get(i).toPath(width - 2 * edge, height - 2 * edge, numSample); + canvas.drawPath(path, paint); + } + + return bitmap; + } + + /** + * Create a bitmap of the gesture with a transparent background + * + * @param width + * @param height + * @param edge + * @param color + * @return the bitmap + */ + public Bitmap toBitmap(int width, int height, int edge, int color) { + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + canvas.translate(edge, edge); + + final Paint paint = new Paint(); + paint.setAntiAlias(BITMAP_RENDERING_ANTIALIAS); + paint.setDither(BITMAP_RENDERING_DITHER); + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(BITMAP_RENDERING_WIDTH); + + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + strokes.get(i).draw(canvas, paint); + } + + return bitmap; + } + + void serialize(DataOutputStream out) throws IOException { + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + // Write gesture ID + out.writeLong(mGestureID); + // Write number of strokes + out.writeInt(count); + + for (int i = 0; i < count; i++) { + strokes.get(i).serialize(out); + } + } + + static Gesture deserialize(DataInputStream in) throws IOException { + final Gesture gesture = new Gesture(); + + // Gesture ID + gesture.mGestureID = in.readLong(); + // Number of strokes + final int count = in.readInt(); + + for (int i = 0; i < count; i++) { + gesture.addStroke(GestureStroke.deserialize(in)); + } + + return gesture; + } + + public static final Parcelable.Creator<Gesture> CREATOR = new Parcelable.Creator<Gesture>() { + public Gesture createFromParcel(Parcel in) { + Gesture gesture = null; + final long gestureID = in.readLong(); + + final DataInputStream inStream = new DataInputStream( + new ByteArrayInputStream(in.createByteArray())); + + try { + gesture = deserialize(inStream); + } catch (IOException e) { + Log.e(GestureConstants.LOG_TAG, "Error reading Gesture from parcel:", e); + } finally { + GestureUtilities.closeStream(inStream); + } + + if (gesture != null) { + gesture.mGestureID = gestureID; + } + + return gesture; + } + + public Gesture[] newArray(int size) { + return new Gesture[size]; + } + }; + + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mGestureID); + + boolean result = false; + final ByteArrayOutputStream byteStream = + new ByteArrayOutputStream(GestureConstants.IO_BUFFER_SIZE); + final DataOutputStream outStream = new DataOutputStream(byteStream); + + try { + serialize(outStream); + result = true; + } catch (IOException e) { + Log.e(GestureConstants.LOG_TAG, "Error writing Gesture to parcel:", e); + } finally { + GestureUtilities.closeStream(outStream); + GestureUtilities.closeStream(byteStream); + } + + if (result) { + out.writeByteArray(byteStream.toByteArray()); + } + } + + public int describeContents() { + return 0; + } +} + diff --git a/core/java/android/gesture/GestureConstants.java b/core/java/android/gesture/GestureConstants.java new file mode 100644 index 0000000..230db0c --- /dev/null +++ b/core/java/android/gesture/GestureConstants.java @@ -0,0 +1,26 @@ +/* + * 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; + +interface GestureConstants { + static final int STROKE_STRING_BUFFER_SIZE = 1024; + static final int STROKE_POINT_BUFFER_SIZE = 100; // number of points + + static final int IO_BUFFER_SIZE = 32 * 1024; // 32K + + static final String LOG_TAG = "Gestures"; +} diff --git a/core/java/android/gesture/GestureLibrary.java b/core/java/android/gesture/GestureLibrary.java new file mode 100644 index 0000000..1cf192e --- /dev/null +++ b/core/java/android/gesture/GestureLibrary.java @@ -0,0 +1,346 @@ +/* + * 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 android.gesture; + +import android.util.Log; +import android.os.SystemClock; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.DataOutputStream; +import java.io.DataInputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; +import java.util.Map; + +import static android.gesture.GestureConstants.LOG_TAG; + +/** + * GestureLibrary maintains gesture examples and makes predictions on a new + * gesture + */ +// +// File format for GestureLibrary: +// +// Nb. bytes Java type Description +// ----------------------------------- +// Header +// 2 bytes short File format version number +// 4 bytes int Number of entries +// Entry +// X bytes UTF String Entry name +// 4 bytes int Number of gestures +// Gesture +// 8 bytes long Gesture ID +// 4 bytes int Number of strokes +// Stroke +// 4 bytes int Number of points +// Point +// 4 bytes float X coordinate of the point +// 4 bytes float Y coordinate of the point +// 8 bytes long Time stamp +// +public class GestureLibrary { + public static final int SEQUENCE_INVARIANT = 1; + // when SEQUENCE_SENSITIVE is used, only single stroke gestures are currently allowed + public static final int SEQUENCE_SENSITIVE = 2; + + // ORIENTATION_SENSITIVE and ORIENTATION_INVARIANT are only for SEQUENCE_SENSITIVE gestures + public static final int ORIENTATION_INVARIANT = 1; + public static final int ORIENTATION_SENSITIVE = 2; + + private static final short FILE_FORMAT_VERSION = 1; + + private static final boolean PROFILE_LOADING_SAVING = false; + + private int mSequenceType = SEQUENCE_SENSITIVE; + private int mOrientationStyle = ORIENTATION_SENSITIVE; + + private final String mGestureFileName; + + private final HashMap<String, ArrayList<Gesture>> mNamedGestures = + new HashMap<String, ArrayList<Gesture>>(); + + private Learner mClassifier; + + private boolean mChanged = false; + + /** + * @param path where gesture data is stored + */ + public GestureLibrary(String path) { + mGestureFileName = path; + mClassifier = new InstanceLearner(); + } + + /** + * Specify how the gesture library will handle orientation. + * Use ORIENTATION_INVARIANT or ORIENTATION_SENSITIVE + * + * @param style + */ + public void setOrientationStyle(int style) { + mOrientationStyle = style; + } + + public int getOrientationStyle() { + return mOrientationStyle; + } + + /** + * @param type SEQUENCE_INVARIANT or SEQUENCE_SENSITIVE + */ + public void setSequenceType(int type) { + mSequenceType = type; + } + + /** + * @return SEQUENCE_INVARIANT or SEQUENCE_SENSITIVE + */ + public int getSequenceType() { + return mSequenceType; + } + + /** + * Get all the gesture entry names in the library + * + * @return a set of strings + */ + public Set<String> getGestureEntries() { + return mNamedGestures.keySet(); + } + + /** + * Recognize a gesture + * + * @param gesture the query + * @return a list of predictions of possible entries for a given gesture + */ + public ArrayList<Prediction> recognize(Gesture gesture) { + Instance instance = Instance.createInstance(mSequenceType, gesture, null); + return mClassifier.classify(mSequenceType, instance.vector); + } + + /** + * Add a gesture for the entry + * + * @param entryName entry name + * @param gesture + */ + public void addGesture(String entryName, Gesture gesture) { + if (entryName == null || entryName.length() == 0) { + return; + } + ArrayList<Gesture> gestures = mNamedGestures.get(entryName); + if (gestures == null) { + gestures = new ArrayList<Gesture>(); + mNamedGestures.put(entryName, gestures); + } + gestures.add(gesture); + mClassifier.addInstance(Instance.createInstance(mSequenceType, gesture, entryName)); + mChanged = true; + } + + /** + * Remove a gesture from the library. If there are no more gestures for the + * given entry, the gesture entry will be removed. + * + * @param entryName entry name + * @param gesture + */ + public void removeGesture(String entryName, Gesture gesture) { + ArrayList<Gesture> gestures = mNamedGestures.get(entryName); + if (gestures == null) { + return; + } + + gestures.remove(gesture); + + // if there are no more samples, remove the entry automatically + if (gestures.isEmpty()) { + mNamedGestures.remove(entryName); + } + + mClassifier.removeInstance(gesture.getID()); + + mChanged = true; + } + + /** + * Remove a entry of gestures + * + * @param entryName the entry name + */ + public void removeEntry(String entryName) { + mNamedGestures.remove(entryName); + mClassifier.removeInstances(entryName); + mChanged = true; + } + + /** + * Get all the gestures of an entry + * + * @param entryName + * @return the list of gestures that is under this name + */ + public ArrayList<Gesture> getGestures(String entryName) { + ArrayList<Gesture> gestures = mNamedGestures.get(entryName); + if (gestures != null) { + return new ArrayList<Gesture>(gestures); + } else { + return null; + } + } + + /** + * Save the gesture library + */ + public boolean save() { + if (!mChanged) { + return true; + } + + boolean result = false; + DataOutputStream out = null; + + try { + File file = new File(mGestureFileName); + if (!file.getParentFile().exists()) { + if (!file.getParentFile().mkdirs()) { + return false; + } + } + + long start; + if (PROFILE_LOADING_SAVING) { + start = SystemClock.elapsedRealtime(); + } + + final HashMap<String, ArrayList<Gesture>> maps = mNamedGestures; + + out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file), + GestureConstants.IO_BUFFER_SIZE)); + // Write version number + out.writeShort(FILE_FORMAT_VERSION); + // Write number of entries + out.writeInt(maps.size()); + + for (Map.Entry<String, ArrayList<Gesture>> entry : maps.entrySet()) { + final String key = entry.getKey(); + final ArrayList<Gesture> examples = entry.getValue(); + final int count = examples.size(); + + // Write entry name + out.writeUTF(key); + // Write number of examples for this entry + out.writeInt(count); + + for (int i = 0; i < count; i++) { + examples.get(i).serialize(out); + } + } + + out.flush(); + + if (PROFILE_LOADING_SAVING) { + long end = SystemClock.elapsedRealtime(); + Log.d(LOG_TAG, "Saving gestures library = " + (end - start) + " ms"); + } + + mChanged = false; + result = true; + } catch (IOException ex) { + Log.d(LOG_TAG, "Failed to save gestures:", ex); + } finally { + GestureUtilities.closeStream(out); + } + + return result; + } + + /** + * Load the gesture library + */ + public boolean load() { + boolean result = false; + + final File file = new File(mGestureFileName); + if (file.exists()) { + DataInputStream in = null; + try { + in = new DataInputStream(new BufferedInputStream( + new FileInputStream(mGestureFileName), GestureConstants.IO_BUFFER_SIZE)); + + long start; + if (PROFILE_LOADING_SAVING) { + start = SystemClock.elapsedRealtime(); + } + + // Read file format version number + final short versionNumber = in.readShort(); + switch (versionNumber) { + case 1: + readFormatV1(in); + break; + } + + if (PROFILE_LOADING_SAVING) { + long end = SystemClock.elapsedRealtime(); + Log.d(LOG_TAG, "Loading gestures library = " + (end - start) + " ms"); + } + + result = true; + } catch (IOException ex) { + Log.d(LOG_TAG, "Failed to load gestures:", ex); + } finally { + GestureUtilities.closeStream(in); + } + } + + return result; + } + + private void readFormatV1(DataInputStream in) throws IOException { + final Learner classifier = mClassifier; + final HashMap<String, ArrayList<Gesture>> namedGestures = mNamedGestures; + namedGestures.clear(); + + // Number of entries in the library + final int entriesCount = in.readInt(); + + for (int i = 0; i < entriesCount; i++) { + // Entry name + final String name = in.readUTF(); + // Number of gestures + final int gestureCount = in.readInt(); + + final ArrayList<Gesture> gestures = new ArrayList<Gesture>(gestureCount); + for (int j = 0; j < gestureCount; j++) { + final Gesture gesture = Gesture.deserialize(in); + gestures.add(gesture); + classifier.addInstance(Instance.createInstance(mSequenceType, gesture, name)); + } + + namedGestures.put(name, gestures); + } + } +} diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java new file mode 100755 index 0000000..bffd12e --- /dev/null +++ b/core/java/android/gesture/GestureOverlayView.java @@ -0,0 +1,419 @@ +/* + * 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 android.gesture; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BlurMaskFilter; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.Color; +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 GestureOverlayView extends View { + static final float TOUCH_TOLERANCE = 3; + + // TODO: Move all these values into XML attributes + private static final int TRANSPARENT_BACKGROUND = 0x00000000; + + // TODO: SHOULD BE A TOTAL DURATION + private static final float FADING_ALPHA_CHANGE = 0.15f; + private static final long FADING_OFFSET = 300; + private static final long FADING_REFRESHING_RATE = 16; + + private static final int GESTURE_STROKE_WIDTH = 12; + private static final boolean GESTURE_RENDERING_ANTIALIAS = true; + + private static final boolean DITHER_FLAG = true; + + public static final int DEFAULT_GESTURE_COLOR = 0xFFFFFF00; + public static final int DEFAULT_UNCERTAIN_GESTURE_COLOR = Color.argb(60, 255, 255, 0); + + private static final int REFRESH_RANGE = 10; + + private static final BlurMaskFilter BLUR_MASK_FILTER = + new BlurMaskFilter(1, BlurMaskFilter.Blur.NORMAL); + + private Paint mGesturePaint; + + private final Paint mBitmapPaint = new Paint(Paint.DITHER_FLAG); + private Bitmap mBitmap; // with transparent background + private Canvas mBitmapCanvas; + + private int mCertainGestureColor = DEFAULT_GESTURE_COLOR; + private int mUncertainGestureColor = DEFAULT_UNCERTAIN_GESTURE_COLOR; + + // 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; + + // TODO: Make this a list of WeakReferences + private final ArrayList<OnGestureListener> mOnGestureListeners = new ArrayList<OnGestureListener>(); + private ArrayList<GesturePoint> mPointBuffer = null; + + // fading out effect + private boolean mIsFadingOut = false; + private float mFadingAlpha = 1; + + private Handler mHandler = new Handler(); + + private final 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 GestureOverlayView(Context context) { + super(context); + init(); + } + + public GestureOverlayView(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 setGestureDrawingColor(int color) { + mGesturePaint.setColor(color); + if (mCurrentGesture != null) { + mBitmap.eraseColor(TRANSPARENT_BACKGROUND); + mCurrentGesture.draw(mBitmapCanvas, mGesturePaint); + } + } + + public void setGestureColor(int color) { + mCertainGestureColor = color; + } + + public void setUncertainGestureColor(int color) { + mUncertainGestureColor = color; + } + + public int getUncertainGestureColor() { + return mUncertainGestureColor; + } + + public int getGestureColor() { + return mCertainGestureColor; + } + + /** + * 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(); + + final Paint gesturePaint = mGesturePaint; + gesturePaint.setAntiAlias(GESTURE_RENDERING_ANTIALIAS); + gesturePaint.setColor(DEFAULT_GESTURE_COLOR); + gesturePaint.setStyle(Paint.Style.STROKE); + gesturePaint.setStrokeJoin(Paint.Join.ROUND); + gesturePaint.setStrokeCap(Paint.Cap.ROUND); + gesturePaint.setStrokeWidth(GESTURE_STROKE_WIDTH); + gesturePaint.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; + + if (mBitmap != null) mBitmap.recycle(); + + mBitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888); + if (mBitmapCanvas != null) { + mBitmapCanvas.setBitmap(mBitmap); + } else { + mBitmapCanvas = new Canvas(mBitmap); + } + mBitmapCanvas.drawColor(TRANSPARENT_BACKGROUND); + + if (mCurrentGesture != null) { + mCurrentGesture.draw(mBitmapCanvas, mGesturePaint); + } + } + + public void addOnGestureListener(OnGestureListener listener) { + mOnGestureListeners.add(listener); + } + + public void removeOnGestureListener(OnGestureListener listener) { + mOnGestureListeners.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_OFFSET); + } 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 + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + OnGestureListener listener = listeners.get(i); + listener.onGestureStarted(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 + 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) { + // 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 + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureEnded(this, event); + } + + mPath = null; + mPointBuffer = null; + } + + /** + * An interface for processing gesture events + */ + public static interface OnGestureListener { + public void onGestureStarted(GestureOverlayView overlay, MotionEvent event); + + public void onGesture(GestureOverlayView overlay, MotionEvent event); + + public void onGestureEnded(GestureOverlayView overlay, MotionEvent event); + } +} diff --git a/core/java/android/gesture/GesturePoint.java b/core/java/android/gesture/GesturePoint.java new file mode 100644 index 0000000..3698011 --- /dev/null +++ b/core/java/android/gesture/GesturePoint.java @@ -0,0 +1,46 @@ +/* + * 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 android.gesture; + +import java.io.DataInputStream; +import java.io.IOException; + +/** + * A timed point of a gesture stroke + */ + +public class GesturePoint { + public final float x; + public final float y; + + public final long timestamp; + + public GesturePoint(float x, float y, long t) { + this.x = x; + this.y = y; + timestamp = t; + } + + static GesturePoint deserialize(DataInputStream in) throws IOException { + // Read X and Y + final float x = in.readFloat(); + final float y = in.readFloat(); + // Read timestamp + final long timeStamp = in.readLong(); + return new GesturePoint(x, y, timeStamp); + } +} diff --git a/core/java/android/gesture/GestureStroke.java b/core/java/android/gesture/GestureStroke.java new file mode 100644 index 0000000..5160a76 --- /dev/null +++ b/core/java/android/gesture/GestureStroke.java @@ -0,0 +1,215 @@ +/* + * 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 android.gesture; + +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; + +import java.io.IOException; +import java.io.DataOutputStream; +import java.io.DataInputStream; +import java.util.ArrayList; + +/** + * A gesture stroke started on a touch down and ended on a touch up. + */ +public class GestureStroke { + public final RectF boundingBox; + + public final float length; + public final float[] points; + + private final long[] timestamps; + private Path mCachedPath; + + /** + * Construct a gesture stroke from a list of gesture points + * + * @param points + */ + public GestureStroke(ArrayList<GesturePoint> points) { + final int count = points.size(); + final float[] tmpPoints = new float[count * 2]; + final long[] times = new long[count]; + + RectF bx = null; + float len = 0; + int index = 0; + + for (int i = 0; i < count; i++) { + final GesturePoint p = points.get(i); + tmpPoints[i * 2] = p.x; + tmpPoints[i * 2 + 1] = p.y; + times[index] = p.timestamp; + + if (bx == null) { + bx = new RectF(); + bx.top = p.y; + bx.left = p.x; + bx.right = p.x; + bx.bottom = p.y; + len = 0; + } else { + len += Math.sqrt(Math.pow(p.x - tmpPoints[(i - 1) * 2], 2) + + Math.pow(p.y - tmpPoints[(i -1 ) * 2 + 1], 2)); + bx.union(p.x, p.y); + } + index++; + } + + timestamps = times; + this.points = tmpPoints; + boundingBox = bx; + length = len; + } + + /** + * Draw the gesture with a given canvas and paint + * + * @param canvas + */ + void draw(Canvas canvas, Paint paint) { + if (mCachedPath == null) { + final float[] localPoints = points; + final int count = localPoints.length; + + Path path = null; + + float mX = 0; + float mY = 0; + + for (int i = 0; i < count; i += 2) { + float x = localPoints[i]; + float y = localPoints[i + 1]; + if (path == null) { + path = new Path(); + path.moveTo(x, y); + mX = x; + mY = y; + } else { + float dx = Math.abs(x - mX); + float dy = Math.abs(y - mY); + if (dx >= 3 || dy >= 3) { + path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); + mX = x; + mY = y; + } + } + } + + mCachedPath = path; + } + + canvas.drawPath(mCachedPath, paint); + } + + /** + * Convert the stroke to a Path based on the number of points + * + * @param width the width of the bounding box of the target path + * @param height the height of the bounding box of the target path + * @param numSample the number of points needed + * + * @return the path + */ + public Path toPath(float width, float height, int numSample) { + final float[] pts = GestureUtilities.temporalSampling(this, numSample); + final RectF rect = boundingBox; + + final Matrix matrix = new Matrix(); + matrix.setTranslate(-rect.left, -rect.top); + matrix.postScale(width / rect.width(), height / rect.height()); + matrix.mapPoints(pts); + + float mX = 0; + float mY = 0; + + Path path = null; + + final int count = pts.length; + + for (int i = 0; i < count; i += 2) { + float x = pts[i]; + float y = pts[i + 1]; + if (path == null) { + path = new Path(); + path.moveTo(x, y); + mX = x; + mY = y; + } else { + float dx = Math.abs(x - mX); + float dy = Math.abs(y - mY); + if (dx >= GestureOverlayView.TOUCH_TOLERANCE || + dy >= GestureOverlayView.TOUCH_TOLERANCE) { + path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); + mX = x; + mY = y; + } + } + } + + return path; + } + + void serialize(DataOutputStream out) throws IOException { + final float[] pts = points; + final long[] times = timestamps; + final int count = points.length; + + // Write number of points + out.writeInt(count / 2); + + for (int i = 0; i < count; i += 2) { + // Write X + out.writeFloat(pts[i]); + // Write Y + out.writeFloat(pts[i + 1]); + // Write timestamp + out.writeLong(times[i / 2]); + } + } + + static GestureStroke deserialize(DataInputStream in) throws IOException { + // Number of points + final int count = in.readInt(); + + final ArrayList<GesturePoint> points = new ArrayList<GesturePoint>(count); + for (int i = 0; i < count; i++) { + points.add(GesturePoint.deserialize(in)); + } + + return new GestureStroke(points); + } + + /** + * Invalidate the cached path that is used to render the stroke + */ + public void clearPath() { + if (mCachedPath != null) mCachedPath.rewind(); + } + + /** + * Compute an oriented bounding box of the stroke + * @return OrientedBoundingBox + */ + public OrientedBoundingBox computeOrientedBoundingBox() { + return GestureUtilities.computeOrientedBoundingBox(points); + } +} diff --git a/core/java/android/gesture/GestureUtilities.java b/core/java/android/gesture/GestureUtilities.java new file mode 100755 index 0000000..e47856c --- /dev/null +++ b/core/java/android/gesture/GestureUtilities.java @@ -0,0 +1,444 @@ +/* + * 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 android.gesture; + +import android.graphics.RectF; +import android.graphics.Matrix; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.io.Closeable; +import java.io.IOException; + +import static android.gesture.GestureConstants.*; + +final class GestureUtilities { + private static final int TEMPORAL_SAMPLING_RATE = 16; + + private GestureUtilities() { + } + + /** + * Closes the specified stream. + * + * @param stream The stream to close. + */ + static void closeStream(Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.e(LOG_TAG, "Could not close stream", e); + } + } + } + + static float[] spatialSampling(Gesture gesture, int sampleMatrixDimension) { + final float targetPatchSize = sampleMatrixDimension - 1; // edge inclusive + float[] sample = new float[sampleMatrixDimension * sampleMatrixDimension]; + Arrays.fill(sample, 0); + + RectF rect = gesture.getBoundingBox(); + float sx = targetPatchSize / rect.width(); + float sy = targetPatchSize / rect.height(); + float scale = sx < sy ? sx : sy; + + Matrix trans = new Matrix(); + trans.setScale(scale, scale); + trans.preTranslate(-rect.centerX(), -rect.centerY()); + trans.postTranslate(targetPatchSize / 2, targetPatchSize / 2); + + final ArrayList<GestureStroke> strokes = gesture.getStrokes(); + final int count = strokes.size(); + + int size; + float xpos; + float ypos; + + for (int index = 0; index < count; index++) { + final GestureStroke stroke = strokes.get(index); + size = stroke.points.length; + + final float[] pts = new float[size]; + + trans.mapPoints(pts, 0, stroke.points, 0, size / 2); + float segmentEndX = -1; + float segmentEndY = -1; + + for (int i = 0; i < size; i += 2) { + + float segmentStartX = pts[i] < 0 ? 0 : pts[i]; + float segmentStartY = pts[i + 1] < 0 ? 0 : pts[i + 1]; + + if (segmentStartX > targetPatchSize) { + segmentStartX = targetPatchSize; + } + + if (segmentStartY > targetPatchSize) { + segmentStartY = targetPatchSize; + } + + plot(segmentStartX, segmentStartY, sample, sampleMatrixDimension); + + if (segmentEndX != -1) { + // evaluate horizontally + if (segmentEndX > segmentStartX) { + xpos = (float) Math.ceil(segmentStartX); + float slope = (segmentEndY - segmentStartY) / (segmentEndX - segmentStartX); + while (xpos < segmentEndX) { + ypos = slope * (xpos - segmentStartX) + segmentStartY; + plot(xpos, ypos, sample, sampleMatrixDimension); + xpos++; + } + } else if (segmentEndX < segmentStartX){ + xpos = (float) Math.ceil(segmentEndX); + float slope = (segmentEndY - segmentStartY) / (segmentEndX - segmentStartX); + while (xpos < segmentStartX) { + ypos = slope * (xpos - segmentStartX) + segmentStartY; + plot(xpos, ypos, sample, sampleMatrixDimension); + xpos++; + } + } + + // evaluating vertically + if (segmentEndY > segmentStartY) { + ypos = (float) Math.ceil(segmentStartY); + float invertSlope = (segmentEndX - segmentStartX) / (segmentEndY - segmentStartY); + while (ypos < segmentEndY) { + xpos = invertSlope * (ypos - segmentStartY) + segmentStartX; + plot(xpos, ypos, sample, sampleMatrixDimension); + ypos++; + } + } else if (segmentEndY < segmentStartY) { + ypos = (float) Math.ceil(segmentEndY); + float invertSlope = (segmentEndX - segmentStartX) / (segmentEndY - segmentStartY); + while (ypos < segmentStartY) { + xpos = invertSlope * (ypos - segmentStartY) + segmentStartX; + plot(xpos, ypos, sample, sampleMatrixDimension); + ypos++; + } + } + } + + segmentEndX = segmentStartX; + segmentEndY = segmentStartY; + } + } + + + return sample; + } + + private static void plot(float x, float y, float[] sample, int sampleSize) { + x = x < 0 ? 0 : x; + y = y < 0 ? 0 : y; + int xFloor = (int) Math.floor(x); + int xCeiling = (int) Math.ceil(x); + int yFloor = (int) Math.floor(y); + int yCeiling = (int) Math.ceil(y); + + // if it's an integer + if (x == xFloor && y == yFloor) { + int index = yCeiling * sampleSize + xCeiling; + if (sample[index] < 1){ + sample[index] = 1; + } + } else { + double topLeft = Math.sqrt(Math.pow(xFloor - x, 2) + Math.pow(yFloor - y, 2)); + double topRight = Math.sqrt(Math.pow(xCeiling - x, 2) + Math.pow(yFloor - y, 2)); + double btmLeft = Math.sqrt(Math.pow(xFloor - x, 2) + Math.pow(yCeiling - y, 2)); + double btmRight = Math.sqrt(Math.pow(xCeiling - x, 2) + Math.pow(yCeiling - y, 2)); + double sum = topLeft + topRight + btmLeft + btmRight; + + double value = topLeft / sum; + int index = yFloor * sampleSize + xFloor; + if (value > sample[index]){ + sample[index] = (float) value; + } + + value = topRight / sum; + index = yFloor * sampleSize + xCeiling; + if (value > sample[index]){ + sample[index] = (float) value; + } + + value = btmLeft / sum; + index = yCeiling * sampleSize + xFloor; + if (value > sample[index]){ + sample[index] = (float) value; + } + + value = btmRight / sum; + index = yCeiling * sampleSize + xCeiling; + if (value > sample[index]){ + sample[index] = (float) value; + } + } + } + + /** + * Featurize a stroke into a vector of a given number of elements + * + * @param stroke + * @param sampleSize + * @return a float array + */ + static float[] temporalSampling(GestureStroke stroke, int sampleSize) { + final float increment = stroke.length / (sampleSize - 1); + int vectorLength = sampleSize * 2; + float[] vector = new float[vectorLength]; + float distanceSoFar = 0; + float[] pts = stroke.points; + float lstPointX = pts[0]; + float lstPointY = pts[1]; + int index = 0; + float currentPointX = Float.MIN_VALUE; + float currentPointY = Float.MIN_VALUE; + vector[index] = lstPointX; + index++; + vector[index] = lstPointY; + index++; + int i = 0; + int count = pts.length / 2; + while (i < count) { + if (currentPointX == Float.MIN_VALUE) { + i++; + if (i >= count) { + break; + } + currentPointX = pts[i * 2]; + currentPointY = pts[i * 2 + 1]; + } + float deltaX = currentPointX - lstPointX; + float deltaY = currentPointY - lstPointY; + float distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY); + if (distanceSoFar + distance >= increment) { + float ratio = (increment - distanceSoFar) / distance; + float nx = lstPointX + ratio * deltaX; + float ny = lstPointY + ratio * deltaY; + vector[index] = nx; + index++; + vector[index] = ny; + index++; + lstPointX = nx; + lstPointY = ny; + distanceSoFar = 0; + } else { + lstPointX = currentPointX; + lstPointY = currentPointY; + currentPointX = Float.MIN_VALUE; + currentPointY = Float.MIN_VALUE; + distanceSoFar += distance; + } + } + + for (i = index; i < vectorLength; i += 2) { + vector[i] = lstPointX; + vector[i + 1] = lstPointY; + } + return vector; + } + + /** + * Calculate the centroid + * + * @param points + * @return the centroid + */ + static float[] computeCentroid(float[] points) { + float centerX = 0; + float centerY = 0; + int count = points.length; + for (int i = 0; i < count; i++) { + centerX += points[i]; + i++; + centerY += points[i]; + } + float[] center = new float[2]; + center[0] = 2 * centerX / count; + center[1] = 2 * centerY / count; + + return center; + } + + /** + * calculate the variance-covariance matrix, treat each point as a sample + * + * @param points + * @return the covariance matrix + */ + private static double[][] computeCoVariance(float[] points) { + double[][] array = new double[2][2]; + array[0][0] = 0; + array[0][1] = 0; + array[1][0] = 0; + array[1][1] = 0; + int count = points.length; + for (int i = 0; i < count; i++) { + float x = points[i]; + i++; + float y = points[i]; + array[0][0] += x * x; + array[0][1] += x * y; + array[1][0] = array[0][1]; + array[1][1] += y * y; + } + array[0][0] /= (count / 2); + array[0][1] /= (count / 2); + array[1][0] /= (count / 2); + array[1][1] /= (count / 2); + + return array; + } + + static float computeTotalLength(float[] points) { + float sum = 0; + int count = points.length - 4; + for (int i = 0; i < count; i += 2) { + float dx = points[i + 2] - points[i]; + float dy = points[i + 3] - points[i + 1]; + sum += Math.sqrt(dx * dx + dy * dy); + } + return sum; + } + + static double computeStraightness(float[] points) { + float totalLen = computeTotalLength(points); + float dx = points[2] - points[0]; + float dy = points[3] - points[1]; + return Math.sqrt(dx * dx + dy * dy) / totalLen; + } + + static double computeStraightness(float[] points, float totalLen) { + float dx = points[2] - points[0]; + float dy = points[3] - points[1]; + return Math.sqrt(dx * dx + dy * dy) / totalLen; + } + + /** + * Calculate the squared Euclidean distance between two vectors + * + * @param vector1 + * @param vector2 + * @return the distance + */ + static double squaredEuclideanDistance(float[] vector1, float[] vector2) { + double squaredDistance = 0; + int size = vector1.length; + for (int i = 0; i < size; i++) { + float difference = vector1[i] - vector2[i]; + squaredDistance += difference * difference; + } + return squaredDistance / size; + } + + /** + * Calculate the cosine distance between two instances + * + * @param vector1 + * @param vector2 + * @return the distance between 0 and Math.PI + */ + static double cosineDistance(float[] vector1, float[] vector2) { + float sum = 0; + int len = vector1.length; + for (int i = 0; i < len; i++) { + sum += vector1[i] * vector2[i]; + } + return Math.acos(sum); + } + + static OrientedBoundingBox computeOrientedBoundingBox(ArrayList<GesturePoint> pts) { + GestureStroke stroke = new GestureStroke(pts); + float[] points = temporalSampling(stroke, TEMPORAL_SAMPLING_RATE); + return computeOrientedBoundingBox(points); + } + + static OrientedBoundingBox computeOrientedBoundingBox(float[] points) { + float[] meanVector = computeCentroid(points); + return computeOrientedBoundingBox(points, meanVector); + } + + static OrientedBoundingBox computeOrientedBoundingBox(float[] points, float[] centroid) { + Matrix tr = new Matrix(); + tr.setTranslate(-centroid[0], -centroid[1]); + tr.mapPoints(points); + + double[][] array = computeCoVariance(points); + double[] targetVector = computeOrientation(array); + + float angle; + if (targetVector[0] == 0 && targetVector[1] == 0) { + angle = -90; + } else { // -PI<alpha<PI + angle = (float) Math.atan2(targetVector[1], targetVector[0]); + angle = (float) (180 * angle / Math.PI); + android.graphics.Matrix trans = new android.graphics.Matrix(); + trans.setRotate(-angle); + trans.mapPoints(points); + } + + float minx = Float.MAX_VALUE; + float miny = Float.MAX_VALUE; + float maxx = Float.MIN_VALUE; + float maxy = Float.MIN_VALUE; + int count = points.length; + for (int i = 0; i < count; i++) { + if (points[i] < minx) { + minx = points[i]; + } + if (points[i] > maxx) { + maxx = points[i]; + } + i++; + if (points[i] < miny) { + miny = points[i]; + } + if (points[i] > maxy) { + maxy = points[i]; + } + } + + return new OrientedBoundingBox(angle, centroid[0], centroid[1], maxx - minx, maxy - miny); + } + + private static double[] computeOrientation(double[][] covarianceMatrix) { + double[] targetVector = new double[2]; + if (covarianceMatrix[0][1] == 0 || covarianceMatrix[1][0] == 0) { + targetVector[0] = 1; + targetVector[1] = 0; + } + + double a = -covarianceMatrix[0][0] - covarianceMatrix[1][1]; + double b = covarianceMatrix[0][0] * covarianceMatrix[1][1] - covarianceMatrix[0][1] + * covarianceMatrix[1][0]; + double value = a / 2; + double rightside = Math.sqrt(Math.pow(value, 2) - b); + double lambda1 = -value + rightside; + double lambda2 = -value - rightside; + if (lambda1 == lambda2) { + targetVector[0] = 0; + targetVector[1] = 0; + } else { + double lambda = lambda1 > lambda2 ? lambda1 : lambda2; + targetVector[0] = 1; + targetVector[1] = (lambda - covarianceMatrix[0][0]) / covarianceMatrix[0][1]; + } + return targetVector; + } +} diff --git a/core/java/android/gesture/Instance.java b/core/java/android/gesture/Instance.java new file mode 100755 index 0000000..7922fab --- /dev/null +++ b/core/java/android/gesture/Instance.java @@ -0,0 +1,115 @@ +/* + * 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 android.gesture; + +import android.graphics.Matrix; + +/** + * An instance represents a sample if the label is available or a query if the + * label is null. + */ +class Instance { + private static final int SEQUENCE_SAMPLE_SIZE = 16; + + private static final int PATCH_SAMPLE_SIZE = 16; + + private final static float[] ORIENTATIONS = { + 0, 45, 90, 135, 180, -0, -45, -90, -135, -180 + }; + + // the feature vector + final float[] vector; + + // the label can be null + final String label; + + // the id of the instance + final long id; + + private Instance(long id, float[] sample, String sampleName) { + this.id = id; + vector = sample; + label = sampleName; + } + + private void normalize() { + float[] sample = vector; + float sum = 0; + + int size = sample.length; + for (int i = 0; i < size; i++) { + sum += sample[i] * sample[i]; + } + + float magnitude = (float) Math.sqrt(sum); + for (int i = 0; i < size; i++) { + sample[i] /= magnitude; + } + } + + /** + * create a learning instance for a single stroke gesture + * + * @param gesture + * @param label + * @return the instance + */ + static Instance createInstance(int samplingType, Gesture gesture, String label) { + float[] pts; + Instance instance; + if (samplingType == GestureLibrary.SEQUENCE_SENSITIVE) { + pts = temporalSampler(samplingType, gesture); + instance = new Instance(gesture.getID(), pts, label); + instance.normalize(); + } else { + pts = spatialSampler(gesture); + instance = new Instance(gesture.getID(), pts, label); + } + return instance; + } + + private static float[] spatialSampler(Gesture gesture) { + return GestureUtilities.spatialSampling(gesture, PATCH_SAMPLE_SIZE); + } + + private static float[] temporalSampler(int samplingType, Gesture gesture) { + float[] pts = GestureUtilities.temporalSampling(gesture.getStrokes().get(0), + SEQUENCE_SAMPLE_SIZE); + float[] center = GestureUtilities.computeCentroid(pts); + float orientation = (float) Math.atan2(pts[1] - center[1], pts[0] - center[0]); + orientation *= 180 / Math.PI; + + float adjustment = -orientation; + if (samplingType == GestureLibrary.ORIENTATION_SENSITIVE) { + int count = ORIENTATIONS.length; + for (int i = 0; i < count; i++) { + float delta = ORIENTATIONS[i] - orientation; + if (Math.abs(delta) < Math.abs(adjustment)) { + adjustment = delta; + } + } + } + + Matrix m = new Matrix(); + m.setTranslate(-center[0], -center[1]); + m.postRotate(adjustment); + m.mapPoints(pts); + + return pts; + } + +} diff --git a/core/java/android/gesture/InstanceLearner.java b/core/java/android/gesture/InstanceLearner.java new file mode 100644 index 0000000..1739cdc --- /dev/null +++ b/core/java/android/gesture/InstanceLearner.java @@ -0,0 +1,91 @@ +/* + * 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 android.gesture; + +import android.util.Config; +import android.util.Log; +import static android.gesture.GestureConstants.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.TreeMap; + +/** + * An implementation of an instance-based learner + */ + +class InstanceLearner extends Learner { + @Override + ArrayList<Prediction> classify(int gestureType, float[] vector) { + ArrayList<Prediction> predictions = new ArrayList<Prediction>(); + ArrayList<Instance> instances = getInstances(); + int count = instances.size(); + TreeMap<String, Double> label2score = new TreeMap<String, Double>(); + for (int i = 0; i < count; i++) { + Instance sample = instances.get(i); + if (sample.vector.length != vector.length) { + continue; + } + double distance; + if (gestureType == GestureLibrary.SEQUENCE_SENSITIVE) { + distance = GestureUtilities.cosineDistance(sample.vector, vector); + } else { + distance = GestureUtilities.squaredEuclideanDistance(sample.vector, vector); + } + double weight; + if (distance == 0) { + weight = Double.MAX_VALUE; + } else { + weight = 1 / distance; + } + Double score = label2score.get(sample.label); + if (score == null || weight > score) { + label2score.put(sample.label, weight); + } + } + + double sum = 0; + for (String name : label2score.keySet()) { + double score = label2score.get(name); + sum += score; + predictions.add(new Prediction(name, score)); + } + + // normalize + for (Prediction prediction : predictions) { + prediction.score /= sum; + } + + Collections.sort(predictions, new Comparator<Prediction>() { + public int compare(Prediction object1, Prediction object2) { + double score1 = object1.score; + double score2 = object2.score; + if (score1 > score2) { + return -1; + } else if (score1 < score2) { + return 1; + } else { + return 0; + } + } + }); + + return predictions; + } +} diff --git a/core/java/android/gesture/Learner.java b/core/java/android/gesture/Learner.java new file mode 100755 index 0000000..feacde5 --- /dev/null +++ b/core/java/android/gesture/Learner.java @@ -0,0 +1,83 @@ +/* + * 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 android.gesture; + +import java.util.ArrayList; + +/** + * The abstract class of a gesture learner + */ +abstract class Learner { + private final ArrayList<Instance> mInstances = new ArrayList<Instance>(); + + /** + * Add an instance to the learner + * + * @param instance + */ + void addInstance(Instance instance) { + mInstances.add(instance); + } + + /** + * Retrieve all the instances + * + * @return instances + */ + ArrayList<Instance> getInstances() { + return mInstances; + } + + /** + * Remove an instance based on its id + * + * @param id + */ + void removeInstance(long id) { + ArrayList<Instance> instances = mInstances; + int count = instances.size(); + for (int i = 0; i < count; i++) { + Instance instance = instances.get(i); + if (id == instance.id) { + instances.remove(instance); + return; + } + } + } + + /** + * Remove all the instances of a category + * + * @param name the category name + */ + void removeInstances(String name) { + final ArrayList<Instance> toDelete = new ArrayList<Instance>(); + final ArrayList<Instance> instances = mInstances; + final int count = instances.size(); + + for (int i = 0; i < count; i++) { + final Instance instance = instances.get(i); + // the label can be null, as specified in Instance + if ((instance.label == null && name == null) || instance.label.equals(name)) { + toDelete.add(instance); + } + } + instances.removeAll(toDelete); + } + + abstract ArrayList<Prediction> classify(int gestureType, float[] vector); +} diff --git a/core/java/android/gesture/LetterRecognizer.java b/core/java/android/gesture/LetterRecognizer.java new file mode 100644 index 0000000..4476746 --- /dev/null +++ b/core/java/android/gesture/LetterRecognizer.java @@ -0,0 +1,273 @@ +/* + * 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.content.Context; +import android.content.res.Resources; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; + +import static android.gesture.GestureConstants.LOG_TAG; + +public class LetterRecognizer { + public final static int RECOGNIZER_LATIN_LOWERCASE = 0; + static final String GESTURE_FILE_NAME = "letters.gestures"; + + private final static int ADJUST_RANGE = 3; + + private SigmoidUnit[] mHiddenLayer; + private SigmoidUnit[] mOutputLayer; + + private final String[] mClasses; + + private final int mPatchSize; + + private GestureLibrary mGestureLibrary; + + private static class SigmoidUnit { + final float[] mWeights; + + private boolean mComputed; + private float mResult; + + SigmoidUnit(float[] weights) { + mWeights = weights; + } + + private float compute(float[] inputs) { + if (!mComputed) { + float sum = 0; + + final int count = inputs.length; + final float[] weights = mWeights; + + for (int i = 0; i < count; i++) { + sum += inputs[i] * weights[i]; + } + sum += weights[weights.length - 1]; + + mResult = 1.0f / (float) (1 + Math.exp(-sum)); + mComputed = true; + } + return mResult; + } + } + + public static LetterRecognizer getLetterRecognizer(Context context, int type) { + switch (type) { + case RECOGNIZER_LATIN_LOWERCASE: { + return createFromResource(context, com.android.internal.R.raw.latin_lowercase); + } + } + return null; + } + + private LetterRecognizer(int numOfInput, int numOfHidden, String[] classes) { + mPatchSize = (int) Math.sqrt(numOfInput); + mHiddenLayer = new SigmoidUnit[numOfHidden]; + mClasses = classes; + mOutputLayer = new SigmoidUnit[classes.length]; + } + + public ArrayList<Prediction> recognize(Gesture gesture) { + float[] query = GestureUtilities.spatialSampling(gesture, mPatchSize); + ArrayList<Prediction> predictions = classify(query); + adjustPrediction(gesture, predictions); + return predictions; + } + + private ArrayList<Prediction> classify(float[] vector) { + final float[] intermediateOutput = compute(mHiddenLayer, vector); + final float[] output = compute(mOutputLayer, intermediateOutput); + final ArrayList<Prediction> predictions = new ArrayList<Prediction>(); + + double sum = 0; + + final String[] classes = mClasses; + final int count = classes.length; + + for (int i = 0; i < count; i++) { + double score = output[i]; + sum += score; + predictions.add(new Prediction(classes[i], score)); + } + + for (int i = 0; i < count; i++) { + predictions.get(i).score /= sum; + } + + Collections.sort(predictions, new Comparator<Prediction>() { + public int compare(Prediction object1, Prediction object2) { + double score1 = object1.score; + double score2 = object2.score; + if (score1 > score2) { + return -1; + } else if (score1 < score2) { + return 1; + } else { + return 0; + } + } + }); + return predictions; + } + + private float[] compute(SigmoidUnit[] layer, float[] input) { + final float[] output = new float[layer.length]; + final int count = layer.length; + + for (int i = 0; i < count; i++) { + output[i] = layer[i].compute(input); + } + + return output; + } + + private static LetterRecognizer createFromResource(Context context, int resourceID) { + final Resources resources = context.getResources(); + + DataInputStream in = null; + LetterRecognizer classifier = null; + + try { + in = new DataInputStream(new BufferedInputStream(resources.openRawResource(resourceID), + GestureConstants.IO_BUFFER_SIZE)); + + final int version = in.readShort(); + + switch (version) { + case 1: + classifier = readV1(in); + break; + } + + } catch (IOException e) { + Log.d(LOG_TAG, "Failed to load handwriting data:", e); + } finally { + GestureUtilities.closeStream(in); + } + + return classifier; + } + + private static LetterRecognizer readV1(DataInputStream in) throws IOException { + + final int iCount = in.readInt(); + final int hCount = in.readInt(); + final int oCount = in.readInt(); + + final String[] classes = new String[oCount]; + for (int i = 0; i < classes.length; i++) { + classes[i] = in.readUTF(); + } + + final LetterRecognizer classifier = new LetterRecognizer(iCount, hCount, classes); + final SigmoidUnit[] hiddenLayer = new SigmoidUnit[hCount]; + final SigmoidUnit[] outputLayer = new SigmoidUnit[oCount]; + + for (int i = 0; i < hCount; i++) { + final float[] weights = new float[iCount + 1]; + for (int j = 0; j <= iCount; j++) { + weights[j] = in.readFloat(); + } + hiddenLayer[i] = new SigmoidUnit(weights); + } + + for (int i = 0; i < oCount; i++) { + final float[] weights = new float[hCount + 1]; + for (int j = 0; j <= hCount; j++) { + weights[j] = in.readFloat(); + } + outputLayer[i] = new SigmoidUnit(weights); + } + + classifier.mHiddenLayer = hiddenLayer; + classifier.mOutputLayer = outputLayer; + + return classifier; + } + + /** + * TODO: Publish this API once we figure out where we should save the personzlied + * gestures, and how to do so across all apps + * + * @hide + */ + public boolean save() { + if (mGestureLibrary != null) { + return mGestureLibrary.save(); + } + return false; + } + + /** + * TODO: Publish this API once we figure out where we should save the personzlied + * gestures, and how to do so across all apps + * + * @hide + */ + public void setPersonalizationEnabled(boolean enabled) { + if (enabled) { + mGestureLibrary = new GestureLibrary(GESTURE_FILE_NAME); + mGestureLibrary.setSequenceType(GestureLibrary.SEQUENCE_INVARIANT); + mGestureLibrary.load(); + } else { + mGestureLibrary = null; + } + } + + /** + * TODO: Publish this API once we figure out where we should save the personzlied + * gestures, and how to do so across all apps + * + * @hide + */ + public void addExample(String letter, Gesture example) { + if (mGestureLibrary != null) { + mGestureLibrary.addGesture(letter, example); + } + } + + private void adjustPrediction(Gesture query, ArrayList<Prediction> predictions) { + if (mGestureLibrary != null) { + final ArrayList<Prediction> results = mGestureLibrary.recognize(query); + final HashMap<String, Prediction> topNList = new HashMap<String, Prediction>(); + + for (int j = 0; j < ADJUST_RANGE; j++) { + Prediction prediction = predictions.remove(0); + topNList.put(prediction.name, prediction); + } + + final int count = results.size(); + for (int j = count - 1; j >= 0 && !topNList.isEmpty(); j--) { + final Prediction item = results.get(j); + final Prediction original = topNList.get(item.name); + if (original != null) { + predictions.add(0, original); + topNList.remove(item.name); + } + } + } + } +} diff --git a/core/java/android/gesture/OrientedBoundingBox.java b/core/java/android/gesture/OrientedBoundingBox.java new file mode 100644 index 0000000..f1335ee --- /dev/null +++ b/core/java/android/gesture/OrientedBoundingBox.java @@ -0,0 +1,85 @@ +/* + * 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 android.gesture; + +import android.graphics.Matrix; +import android.graphics.Path; + +/** + * An oriented bounding box + */ +public class OrientedBoundingBox { + public final float squareness; + + public final float width; + public final float height; + + public final float orientation; + + public final float centerX; + public final float centerY; + + OrientedBoundingBox(float angle, float cx, float cy, float w, float h) { + orientation = angle; + width = w; + height = h; + centerX = cx; + centerY = cy; + float ratio = w / h; + if (ratio > 1) { + squareness = 1 / ratio; + } else { + squareness = ratio; + } + } + + /** + * Currently used for debugging purpose only. + * + * @hide + */ + public Path toPath() { + Path path = new Path(); + float[] point = new float[2]; + point[0] = -width / 2; + point[1] = height / 2; + Matrix matrix = new Matrix(); + matrix.setRotate(orientation); + matrix.postTranslate(centerX, centerY); + matrix.mapPoints(point); + path.moveTo(point[0], point[1]); + + point[0] = -width / 2; + point[1] = -height / 2; + matrix.mapPoints(point); + path.lineTo(point[0], point[1]); + + point[0] = width / 2; + point[1] = -height / 2; + matrix.mapPoints(point); + path.lineTo(point[0], point[1]); + + point[0] = width / 2; + point[1] = height / 2; + matrix.mapPoints(point); + path.lineTo(point[0], point[1]); + + path.close(); + + return path; + } +} diff --git a/core/java/android/gesture/Prediction.java b/core/java/android/gesture/Prediction.java new file mode 100755 index 0000000..ce6ad57 --- /dev/null +++ b/core/java/android/gesture/Prediction.java @@ -0,0 +1,33 @@ +/* + * 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 android.gesture; + +public class Prediction { + public final String name; + + public double score; + + Prediction(String label, double predictionScore) { + name = label; + score = predictionScore; + } + + @Override + public String toString() { + return name; + } +} diff --git a/core/java/android/gesture/TouchThroughGestureListener.java b/core/java/android/gesture/TouchThroughGestureListener.java new file mode 100644 index 0000000..7621ddf --- /dev/null +++ b/core/java/android/gesture/TouchThroughGestureListener.java @@ -0,0 +1,171 @@ +/* + * 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 android.gesture; + +import android.view.MotionEvent; +import android.view.View; + +import java.util.ArrayList; +import java.lang.ref.WeakReference; + +/** + * TouchThroughGesturing implements the interaction behavior that allows a user + * to gesture over a regular UI widget such as ListView and at the same time, + * still allows a user to perform basic interactions (clicking, scrolling and panning) + * with the underlying widget. + */ +public class TouchThroughGestureListener implements GestureOverlayView.OnGestureListener { + public static final int SINGLE_STROKE = 0; + public static final int MULTIPLE_STROKE = 1; + + // TODO: Add properties for all these + private static final float STROKE_LENGTH_THRESHOLD = 30; + private static final float SQUARENESS_THRESHOLD = 0.275f; + private static final float ANGLE_THRESHOLD = 40; + + private boolean mIsGesturing = false; + + private float mTotalLength; + + private float mX; + private float mY; + + private WeakReference<View> mModel; + + private int mGestureType = SINGLE_STROKE; + + // TODO: Use WeakReferences + private final ArrayList<OnGesturePerformedListener> mPerformedListeners = + new ArrayList<OnGesturePerformedListener>(); + + private boolean mStealEvents = false; + + public TouchThroughGestureListener(View model) { + this(model, false); + } + + public TouchThroughGestureListener(View model, boolean stealEvents) { + mModel = new WeakReference<View>(model); + mStealEvents = stealEvents; + } + + /** + * + * @param type SINGLE_STROKE or MULTIPLE_STROKE + */ + public void setGestureType(int type) { + mGestureType = type; + } + + public void onGestureStarted(GestureOverlayView overlay, MotionEvent event) { + if (mGestureType == MULTIPLE_STROKE) { + overlay.cancelFadingOut(); + } + + mX = event.getX(); + mY = event.getY(); + mTotalLength = 0; + mIsGesturing = false; + + if (mGestureType == SINGLE_STROKE || overlay.getCurrentGesture() == null + || overlay.getCurrentGesture().getStrokesCount() == 0) { + overlay.setGestureDrawingColor(overlay.getUncertainGestureColor()); + } + + dispatchEventToModel(event); + } + + private void dispatchEventToModel(MotionEvent event) { + View v = mModel.get(); + if (v != null) v.dispatchTouchEvent(event); + } + + public void onGesture(GestureOverlayView overlay, MotionEvent event) { + //noinspection PointlessBooleanExpression + if (!mStealEvents) { + dispatchEventToModel(event); + } + + if (mIsGesturing) { + return; + } + + final float x = event.getX(); + final float y = event.getY(); + final float dx = x - mX; + final float dy = y - mY; + + mTotalLength += (float) Math.sqrt(dx * dx + dy * dy); + mX = x; + mY = y; + + if (mTotalLength > STROKE_LENGTH_THRESHOLD) { + final OrientedBoundingBox box = + GestureUtilities.computeOrientedBoundingBox(overlay.getCurrentStroke()); + float angle = Math.abs(box.orientation); + if (angle > 90) { + angle = 180 - angle; + } + if (box.squareness > SQUARENESS_THRESHOLD || angle < ANGLE_THRESHOLD) { + mIsGesturing = true; + overlay.setGestureDrawingColor(overlay.getGestureColor()); + if (mStealEvents) { + event = MotionEvent.obtain(event.getDownTime(), System.currentTimeMillis(), + MotionEvent.ACTION_UP, x, y, event.getPressure(), event.getSize(), + event.getMetaState(), event.getXPrecision(), event.getYPrecision(), + event.getDeviceId(), event.getEdgeFlags()); + } + } + } + + if (mStealEvents) { + dispatchEventToModel(event); + } + } + + public void onGestureEnded(GestureOverlayView overlay, MotionEvent event) { + if (mIsGesturing) { + overlay.clear(true); + + final ArrayList<OnGesturePerformedListener> listeners = mPerformedListeners; + final int count = listeners.size(); + + for (int i = 0; i < count; i++) { + listeners.get(i).onGesturePerformed(overlay, overlay.getCurrentGesture()); + } + } else { + dispatchEventToModel(event); + overlay.clear(false); + } + } + + public void addOnGestureActionListener(OnGesturePerformedListener listener) { + mPerformedListeners.add(listener); + } + + public void removeOnGestureActionListener(OnGesturePerformedListener listener) { + mPerformedListeners.remove(listener); + } + + public boolean isGesturing() { + return mIsGesturing; + } + + public static interface OnGesturePerformedListener { + public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture); + } +} |