summaryrefslogtreecommitdiffstats
path: root/core/java/android/gesture
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/gesture')
-rwxr-xr-xcore/java/android/gesture/Gesture.java341
-rw-r--r--core/java/android/gesture/GestureConstants.java26
-rw-r--r--core/java/android/gesture/GestureLibraries.java143
-rw-r--r--core/java/android/gesture/GestureLibrary.java81
-rwxr-xr-xcore/java/android/gesture/GestureOverlayView.java793
-rw-r--r--core/java/android/gesture/GesturePoint.java46
-rw-r--r--core/java/android/gesture/GestureStore.java330
-rw-r--r--core/java/android/gesture/GestureStroke.java229
-rwxr-xr-xcore/java/android/gesture/GestureUtilities.java475
-rwxr-xr-xcore/java/android/gesture/Instance.java113
-rw-r--r--core/java/android/gesture/InstanceLearner.java88
-rwxr-xr-xcore/java/android/gesture/Learner.java83
-rw-r--r--core/java/android/gesture/OrientedBoundingBox.java85
-rwxr-xr-xcore/java/android/gesture/Prediction.java33
-rw-r--r--core/java/android/gesture/package.html5
15 files changed, 2871 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..2262477
--- /dev/null
+++ b/core/java/android/gesture/Gesture.java
@@ -0,0 +1,341 @@
+/*
+ * 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 final RectF mBoundingBox = new RectF();
+
+ // the same as its instance ID
+ private long mGestureID;
+
+ private final ArrayList<GestureStroke> mStrokes = new ArrayList<GestureStroke>();
+
+ public Gesture() {
+ mGestureID = GESTURE_ID_BASE + sGestureCount++;
+ }
+
+ void recycle() {
+ mStrokes.clear();
+ mBoundingBox.setEmpty();
+ }
+
+ /**
+ * @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);
+ 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;
+ }
+
+ public Path toPath() {
+ return toPath(null);
+ }
+
+ public Path toPath(Path path) {
+ if (path == null) path = new Path();
+
+ final ArrayList<GestureStroke> strokes = mStrokes;
+ final int count = strokes.size();
+
+ for (int i = 0; i < count; i++) {
+ path.addPath(strokes.get(i).getPath());
+ }
+
+ return path;
+ }
+
+ public Path toPath(int width, int height, int edge, int numSample) {
+ return toPath(null, width, height, edge, numSample);
+ }
+
+ public Path toPath(Path path, int width, int height, int edge, int numSample) {
+ if (path == null) path = new Path();
+
+ final ArrayList<GestureStroke> strokes = mStrokes;
+ final int count = strokes.size();
+
+ for (int i = 0; i < count; i++) {
+ path.addPath(strokes.get(i).toPath(width - 2 * edge, height - 2 * edge, numSample));
+ }
+
+ return path;
+ }
+
+ /**
+ * 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 inset
+ * @param color
+ * @return the bitmap
+ */
+ public Bitmap toBitmap(int width, int height, int inset, int color) {
+ final Bitmap bitmap = Bitmap.createBitmap(width, height,
+ Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+
+ 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 Path path = toPath();
+ final RectF bounds = new RectF();
+ path.computeBounds(bounds, true);
+
+ final float sx = (width - 2 * inset) / bounds.width();
+ final float sy = (height - 2 * inset) / bounds.height();
+ final float scale = sx > sy ? sy : sx;
+ paint.setStrokeWidth(2.0f / scale);
+
+ path.offset(-bounds.left + (width - bounds.width() * scale) / 2.0f,
+ -bounds.top + (height - bounds.height() * scale) / 2.0f);
+
+ canvas.translate(inset, inset);
+ canvas.scale(scale, scale);
+
+ canvas.drawPath(path, 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/GestureLibraries.java b/core/java/android/gesture/GestureLibraries.java
new file mode 100644
index 0000000..6d6c156
--- /dev/null
+++ b/core/java/android/gesture/GestureLibraries.java
@@ -0,0 +1,143 @@
+/*
+ * 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.util.Log;
+import static android.gesture.GestureConstants.*;
+import android.content.Context;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+
+public final class GestureLibraries {
+ private GestureLibraries() {
+ }
+
+ public static GestureLibrary fromFile(String path) {
+ return fromFile(new File(path));
+ }
+
+ public static GestureLibrary fromFile(File path) {
+ return new FileGestureLibrary(path);
+ }
+
+ public static GestureLibrary fromPrivateFile(Context context, String name) {
+ return fromFile(context.getFileStreamPath(name));
+ }
+
+ public static GestureLibrary fromRawResource(Context context, int resourceId) {
+ return new ResourceGestureLibrary(context, resourceId);
+ }
+
+ private static class FileGestureLibrary extends GestureLibrary {
+ private final File mPath;
+
+ public FileGestureLibrary(File path) {
+ mPath = path;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return !mPath.canWrite();
+ }
+
+ public boolean save() {
+ if (!mStore.hasChanged()) return true;
+
+ final File file = mPath;
+
+ final File parentFile = file.getParentFile();
+ if (!parentFile.exists()) {
+ if (!parentFile.mkdirs()) {
+ return false;
+ }
+ }
+
+ boolean result = false;
+ try {
+ //noinspection ResultOfMethodCallIgnored
+ file.createNewFile();
+ mStore.save(new FileOutputStream(file), true);
+ result = true;
+ } catch (FileNotFoundException e) {
+ Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e);
+ } catch (IOException e) {
+ Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e);
+ }
+
+ return result;
+ }
+
+ public boolean load() {
+ boolean result = false;
+ final File file = mPath;
+ if (file.exists() && file.canRead()) {
+ try {
+ mStore.load(new FileInputStream(file), true);
+ result = true;
+ } catch (FileNotFoundException e) {
+ Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e);
+ } catch (IOException e) {
+ Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e);
+ }
+ }
+
+ return result;
+ }
+ }
+
+ private static class ResourceGestureLibrary extends GestureLibrary {
+ private final WeakReference<Context> mContext;
+ private final int mResourceId;
+
+ public ResourceGestureLibrary(Context context, int resourceId) {
+ mContext = new WeakReference<Context>(context);
+ mResourceId = resourceId;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ public boolean save() {
+ return false;
+ }
+
+ public boolean load() {
+ boolean result = false;
+ final Context context = mContext.get();
+ if (context != null) {
+ final InputStream in = context.getResources().openRawResource(mResourceId);
+ try {
+ mStore.load(in, true);
+ result = true;
+ } catch (IOException e) {
+ Log.d(LOG_TAG, "Could not load the gesture library from raw resource " +
+ context.getResources().getResourceName(mResourceId), e);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/core/java/android/gesture/GestureLibrary.java b/core/java/android/gesture/GestureLibrary.java
new file mode 100644
index 0000000..a29c2c8
--- /dev/null
+++ b/core/java/android/gesture/GestureLibrary.java
@@ -0,0 +1,81 @@
+/*
+ * 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 java.util.Set;
+import java.util.ArrayList;
+
+public abstract class GestureLibrary {
+ protected final GestureStore mStore;
+
+ protected GestureLibrary() {
+ mStore = new GestureStore();
+ }
+
+ public abstract boolean save();
+
+ public abstract boolean load();
+
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ public Learner getLearner() {
+ return mStore.getLearner();
+ }
+
+ public void setOrientationStyle(int style) {
+ mStore.setOrientationStyle(style);
+ }
+
+ public int getOrientationStyle() {
+ return mStore.getOrientationStyle();
+ }
+
+ public void setSequenceType(int type) {
+ mStore.setSequenceType(type);
+ }
+
+ public int getSequenceType() {
+ return mStore.getSequenceType();
+ }
+
+ public Set<String> getGestureEntries() {
+ return mStore.getGestureEntries();
+ }
+
+ public ArrayList<Prediction> recognize(Gesture gesture) {
+ return mStore.recognize(gesture);
+ }
+
+ public void addGesture(String entryName, Gesture gesture) {
+ mStore.addGesture(entryName, gesture);
+ }
+
+ public void removeGesture(String entryName, Gesture gesture) {
+ mStore.removeGesture(entryName, gesture);
+ }
+
+ public void removeEntry(String entryName) {
+ mStore.removeEntry(entryName);
+ }
+
+ public ArrayList<Gesture> getGestures(String entryName) {
+ return mStore.getGestures(entryName);
+ }
+}
diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java
new file mode 100755
index 0000000..5bfdcc4
--- /dev/null
+++ b/core/java/android/gesture/GestureOverlayView.java
@@ -0,0 +1,793 @@
+/*
+ * 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.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 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
+ */
+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 defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.GestureOverlayView, defStyle, 0);
+
+ 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(int color) {
+ mCertainGestureColor = color;
+ }
+
+ public void setUncertainGestureColor(int color) {
+ mUncertainGestureColor = color;
+ }
+
+ public int getUncertainGestureColor() {
+ return mUncertainGestureColor;
+ }
+
+ 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() {
+ 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.sqrt(dx * dx + dy * dy);
+
+ if (mTotalLength > mGestureStrokeLengthThreshold) {
+ final OrientedBoundingBox box =
+ GestureUtilities.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);
+ }
+}
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/GestureStore.java b/core/java/android/gesture/GestureStore.java
new file mode 100644
index 0000000..5f1a445
--- /dev/null
+++ b/core/java/android/gesture/GestureStore.java
@@ -0,0 +1,330 @@
+/*
+ * 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.IOException;
+import java.io.DataOutputStream;
+import java.io.DataInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+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 GestureStore:
+//
+// 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 GestureStore {
+ 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 HashMap<String, ArrayList<Gesture>> mNamedGestures =
+ new HashMap<String, ArrayList<Gesture>>();
+
+ private Learner mClassifier;
+
+ private boolean mChanged = false;
+
+ public GestureStore() {
+ 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,
+ mOrientationStyle, 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, mOrientationStyle, 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;
+ }
+ }
+
+ public boolean hasChanged() {
+ return mChanged;
+ }
+
+ /**
+ * Save the gesture library
+ */
+ public void save(OutputStream stream) throws IOException {
+ save(stream, false);
+ }
+
+ public void save(OutputStream stream, boolean closeStream) throws IOException {
+ DataOutputStream out = null;
+
+ try {
+ long start;
+ if (PROFILE_LOADING_SAVING) {
+ start = SystemClock.elapsedRealtime();
+ }
+
+ final HashMap<String, ArrayList<Gesture>> maps = mNamedGestures;
+
+ out = new DataOutputStream((stream instanceof BufferedOutputStream) ? stream :
+ new BufferedOutputStream(stream, 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;
+ } finally {
+ if (closeStream) GestureUtilities.closeStream(out);
+ }
+ }
+
+ /**
+ * Load the gesture library
+ */
+ public void load(InputStream stream) throws IOException {
+ load(stream, false);
+ }
+
+ public void load(InputStream stream, boolean closeStream) throws IOException {
+ DataInputStream in = null;
+ try {
+ in = new DataInputStream((stream instanceof BufferedInputStream) ? stream :
+ new BufferedInputStream(stream, 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");
+ }
+ } finally {
+ if (closeStream) GestureUtilities.closeStream(in);
+ }
+ }
+
+ 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, mOrientationStyle, gesture, name));
+ }
+
+ namedGestures.put(name, gestures);
+ }
+ }
+
+ Learner getLearner() {
+ return mClassifier;
+ }
+}
diff --git a/core/java/android/gesture/GestureStroke.java b/core/java/android/gesture/GestureStroke.java
new file mode 100644
index 0000000..598eb85
--- /dev/null
+++ b/core/java/android/gesture/GestureStroke.java
@@ -0,0 +1,229 @@
+/*
+ * 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.graphics.Canvas;
+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 {
+ static final float TOUCH_TOLERANCE = 8;
+
+ 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) {
+ makePath();
+ }
+
+ canvas.drawPath(mCachedPath, paint);
+ }
+
+ public Path getPath() {
+ if (mCachedPath == null) {
+ makePath();
+ }
+
+ return mCachedPath;
+ }
+
+ private void makePath() {
+ 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 >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
+ path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
+ mX = x;
+ mY = y;
+ }
+ }
+ }
+
+ mCachedPath = path;
+ }
+
+ /**
+ * 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;
+
+ GestureUtilities.translate(pts, -rect.left, -rect.top);
+
+ float sx = width / rect.width();
+ float sy = height / rect.height();
+ float scale = sx > sy ? sy : sx;
+ GestureUtilities.scale(pts, scale, scale);
+
+ 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 >= TOUCH_TOLERANCE || dy >= 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..40d7029
--- /dev/null
+++ b/core/java/android/gesture/GestureUtilities.java
@@ -0,0 +1,475 @@
+/*
+ * 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.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;
+
+ float preDx = -rect.centerX();
+ float preDy = -rect.centerY();
+ float postDx = targetPatchSize / 2;
+ float postDy = 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);
+ float[] strokepoints = stroke.points;
+ size = strokepoints.length;
+
+ final float[] pts = new float[size];
+
+ for (int i = 0; i < size; i += 2) {
+ pts[i] = (strokepoints[i] + preDx) * scale + postDx;
+ pts[i + 1] = (strokepoints[i + 1] + preDy) * scale + postDy;
+ }
+
+ 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) {
+ translate(points, -centroid[0], -centroid[1]);
+
+ double[][] array = computeCoVariance(points);
+ double[] targetVector = computeOrientation(array);
+
+ float angle;
+ if (targetVector[0] == 0 && targetVector[1] == 0) {
+ angle = (float) -Math.PI/2;
+ } else { // -PI<alpha<PI
+ angle = (float) Math.atan2(targetVector[1], targetVector[0]);
+ rotate(points, -angle);
+ }
+
+ 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((float) (angle * 180 / Math.PI), 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;
+ }
+
+
+ static float[] rotate(float[] points, double angle) {
+ double cos = Math.cos(angle);
+ double sin = Math.sin(angle);
+ int size = points.length;
+ for (int i = 0; i < size; i += 2) {
+ float x = (float) (points[i] * cos - points[i + 1] * sin);
+ float y = (float) (points[i] * sin + points[i + 1] * cos);
+ points[i] = x;
+ points[i + 1] = y;
+ }
+ return points;
+ }
+
+ static float[] translate(float[] points, float dx, float dy) {
+ int size = points.length;
+ for (int i = 0; i < size; i += 2) {
+ points[i] += dx;
+ points[i + 1] += dy;
+ }
+ return points;
+ }
+
+ static float[] scale(float[] points, float sx, float sy) {
+ int size = points.length;
+ for (int i = 0; i < size; i += 2) {
+ points[i] *= sx;
+ points[i + 1] *= sy;
+ }
+ return points;
+ }
+}
diff --git a/core/java/android/gesture/Instance.java b/core/java/android/gesture/Instance.java
new file mode 100755
index 0000000..ef208ac
--- /dev/null
+++ b/core/java/android/gesture/Instance.java
@@ -0,0 +1,113 @@
+/*
+ * 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;
+
+
+/**
+ * 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, (float) (Math.PI / 4), (float) (Math.PI / 2), (float) (Math.PI * 3 / 4),
+ (float) Math.PI, -0, (float) (-Math.PI / 4), (float) (-Math.PI / 2),
+ (float) (-Math.PI * 3 / 4), (float) -Math.PI
+ };
+
+ // 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 sequenceType, int orientationType, Gesture gesture, String label) {
+ float[] pts;
+ Instance instance;
+ if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) {
+ pts = temporalSampler(orientationType, 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 orientationType, 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]);
+
+ float adjustment = -orientation;
+ if (orientationType == GestureStore.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;
+ }
+ }
+ }
+
+ GestureUtilities.translate(pts, -center[0], -center[1]);
+ GestureUtilities.rotate(pts, adjustment);
+
+ return pts;
+ }
+
+}
diff --git a/core/java/android/gesture/InstanceLearner.java b/core/java/android/gesture/InstanceLearner.java
new file mode 100644
index 0000000..b93b76f
--- /dev/null
+++ b/core/java/android/gesture/InstanceLearner.java
@@ -0,0 +1,88 @@
+/*
+ * 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;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.TreeMap;
+
+/**
+ * An implementation of an instance-based learner
+ */
+
+class InstanceLearner extends Learner {
+ private static final Comparator<Prediction> sComparator = 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;
+ }
+ }
+ };
+
+ @Override
+ ArrayList<Prediction> classify(int sequenceType, 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 (sequenceType == GestureStore.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, sComparator);
+
+ 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/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/package.html b/core/java/android/gesture/package.html
new file mode 100644
index 0000000..a54a017
--- /dev/null
+++ b/core/java/android/gesture/package.html
@@ -0,0 +1,5 @@
+<HTML>
+<BODY>
+@hide
+</BODY>
+</HTML>