Merge change 279 into donut

* changes:
  Add sketch gesture demo application.
This commit is contained in:
Android (Google) Code Review
2009-04-17 14:20:38 -07:00
17 changed files with 2144 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.gesture"
android:versionCode="1"
android:versionName="1.0.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name="com.android.gesture.example.GestureEntryDemo"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="com.android.gesture.example.GestureLibViewer"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Spinner
android:id="@+id/spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true"
android:prompt="@string/recognition_result"/>
<com.android.gesture.GesturePad
android:id="@+id/drawingpad"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Spinner
android:id="@+id/spinner"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true"
android:prompt="@string/recognition_result"/>
<com.android.gesture.GesturePad
android:id="@+id/drawingpad"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/previous"
android:text="@string/previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:id="@+id/remove"
android:text="@string/remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:id="@+id/next"
android:text="@string/next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<EditText
android:id="@+id/gesturename_edit"
android:layout_height="wrap_content"
android:layout_width="fill_parent"
android:layout_marginLeft="20dip"
android:layout_marginRight="20dip"
android:scrollHorizontally="true"
android:autoText="false"
android:capitalize="none"
android:gravity="fill_horizontal"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2008 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.
-->
<resources>
<string name="app_name">Gesture Demo</string>
<string name="recognition_result">Recognition Result</string>
<string name="clear">Clear</string>
<string name="newgesture">Add</string>
<string name="viewgesture">View</string>
<string name="newgesture_dialog_ok">OK</string>
<string name="newgesture_dialog_cancel">Cancel</string>
<string name="newgesture_text_entry">Gesture Name</string>
<string name="previous">Previous</string>
<string name="remove">Remove</string>
<string name="next">Next</string>
</resources>

View File

@@ -0,0 +1,361 @@
/*
* Copyright (C) 2008-2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.gesture.recognizer.RecognitionUtil;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.StringTokenizer;
/**
* A single stroke gesture.
*/
public class Gesture implements Parcelable {
private RectF mBBX;
private float mLength = 0;
private int mColor;
private float mWidth;
private ArrayList<PointF> mPtsBuffer = new ArrayList<PointF>();
private long mTimestamp = 0;
private long mID;
private static final long systemStartupTime = System.currentTimeMillis();
private static int instanceCount = 0;
public Gesture() {
mID = systemStartupTime + instanceCount++;
}
public void setColor(int c) {
mColor = c;
}
public void setStrokeWidth(float w) {
mWidth = w;
}
public int getColor() {
return mColor;
}
public float getStrokeWidth() {
return mWidth;
}
public ArrayList<PointF> getPoints() {
return this.mPtsBuffer;
}
public int numOfPoints() {
return this.mPtsBuffer.size();
}
public void addPoint(float x, float y) {
mPtsBuffer.add(new PointF(x, y));
if (mBBX == null) {
mBBX = new RectF();
mBBX.top = y;
mBBX.left = x;
mBBX.right = x;
mBBX.bottom = y;
mLength = 0;
}
else {
PointF lst = mPtsBuffer.get(mPtsBuffer.size()-2);
mLength += Math.sqrt(Math.pow(x-lst.x, 2)+Math.pow(y-lst.y, 2));
mBBX.union(x, y);
}
mTimestamp = System.currentTimeMillis();
}
/**
* @return the length of the gesture
*/
public float getLength() {
return this.mLength;
}
public RectF getBBX() {
return mBBX;
}
public void setID(long id) {
mID = id;
}
public long getID() {
return mID;
}
public long getTimeStamp() {
return mTimestamp;
}
public void setTimestamp(long t) {
this.mTimestamp = t;
}
/**
* draw the gesture
* @param canvas
*/
public void draw(Canvas canvas) {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
paint.setColor(mColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(mWidth);
Path path = null;
float mX = 0, mY = 0;
Iterator<PointF> it = mPtsBuffer.iterator();
while (it.hasNext()) {
PointF p = it.next();
float x = p.x;
float y = p.y;
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;
}
}
}
canvas.drawPath(path, paint);
}
/**
* convert the gesture to a Path
* @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 num of points needed
* @return the path
*/
public Path toPath(float width, float height, int numSample) {
float[] pts = RecognitionUtil.resample(this, numSample);
RectF rect = this.getBBX();
float scale = height / rect.height();
Matrix matrix = new Matrix();
matrix.setTranslate(-rect.left, -rect.top);
Matrix scalem = new Matrix();
scalem.setScale(scale, scale);
matrix.postConcat(scalem);
Matrix translate = new Matrix();
matrix.postConcat(translate);
matrix.mapPoints(pts);
Path path = null;
float mX = 0, mY = 0;
for (int i=0; i<pts.length-1; 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 >= 3 || dy >= 3) {
path.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
}
}
}
return path;
}
/**
* get a bitmap thumbnail of the gesture with a transparent background
* @param w
* @param h
* @param edge
* @param numSample
* @param foreground
* @return
*/
public Bitmap toBitmap(int w, int h,
int edge, int numSample) {
RectF bbx = this.getBBX();
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Path path = this.toPath(w - 2 * edge, h - 2 * edge, numSample);
Canvas c = new Canvas(bitmap);
//c.drawColor(background);
c.translate(edge, edge);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
paint.setColor(mColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(2);
c.drawPath(path, paint);
return bitmap;
}
/**
* save the gesture as XML
* @param namespace
* @param serializer
* @throws IOException
*/
public void toXML(String namespace, XmlSerializer serializer) throws IOException {
serializer.startTag(namespace, "stroke");
serializer.attribute(namespace, "timestamp", Long.toString(mTimestamp));
serializer.attribute(namespace, "id", Long.toString(mID));
serializer.attribute(namespace, "color", Integer.toString(mColor));
serializer.attribute(namespace, "width", Float.toString(mWidth));
Iterator it = this.mPtsBuffer.iterator();
String pts = "";
while (it.hasNext()) {
PointF fp = (PointF)it.next();
if (pts.length() > 0)
pts += ",";
pts += fp.x + "," + fp.y;
}
serializer.text(pts);
serializer.endTag(namespace, "stroke");
}
public void createFromString(String str) {
StringTokenizer st = new StringTokenizer(str, "#");
String para = st.nextToken();
StringTokenizer innerst = new StringTokenizer(para, ",");
this.mBBX = new RectF();
this.mBBX.left = Float.parseFloat(innerst.nextToken());
this.mBBX.top = Float.parseFloat(innerst.nextToken());
this.mBBX.right = Float.parseFloat(innerst.nextToken());
this.mBBX.bottom = Float.parseFloat(innerst.nextToken());
para = st.nextToken();
innerst = new StringTokenizer(para, ",");
while (innerst.hasMoreTokens()) {
String s = innerst.nextToken().trim();
if (s.length()==0)
break;
float x = Float.parseFloat(s);
float y = Float.parseFloat(innerst.nextToken());
this.mPtsBuffer.add(new PointF(x, y));
}
para = st.nextToken();
this.mColor = Integer.parseInt(para);
para = st.nextToken();
this.mWidth = Float.parseFloat(para);
para = st.nextToken();
this.mLength = Float.parseFloat(para);
para = st.nextToken();
this.mTimestamp = Long.parseLong(para);
}
@Override
public String toString() {
String str = "";
str += "#" + this.mBBX.left + "," + this.mBBX.top + "," +
this.mBBX.right + "," + this.mBBX.bottom;
str += "#";
Iterator<PointF> it = this.mPtsBuffer.iterator();
while (it.hasNext()) {
PointF fp = it.next();
str += fp.x + "," + fp.y + ",";
}
str += "#";
str += this.mColor;
str += "#";
str += this.mWidth;
str += "#";
str += this.mLength;
str += "#";
str += this.mTimestamp;
return str;
}
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
public Gesture createFromParcel(Parcel in) {
String str = in.readString();
Gesture stk = new Gesture();
stk.createFromString(str);
return stk;
}
public Gesture[] newArray(int size) {
return new Gesture[size];
}
};
public static Gesture buildFromArray(byte[] bytes) {
String str = new String(bytes);
Gesture stk = new Gesture();
stk.createFromString(str);
return stk;
}
public static byte[] saveToArray(Gesture stk) {
String str = stk.toString();
return str.getBytes();
}
public void writeToParcel(Parcel out, int flags) {
out.writeString(this.toString());
}
public int describeContents() {
return CONTENTS_FILE_DESCRIPTOR;
}
}

View File

@@ -0,0 +1,280 @@
/*
* Copyright (C) 2008-2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture;
import android.util.Log;
import android.util.Xml;
import android.util.Xml.Encoding;
import com.android.gesture.recognizer.Classifier;
import com.android.gesture.recognizer.Instance;
import com.android.gesture.recognizer.NearestNeighbor;
import com.android.gesture.recognizer.Prediction;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlSerializer;
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.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;
public class GestureLib {
private static final String LOGTAG = "GestureLib";
private static String namespace = "ink";
private final String datapath;
private HashMap<String, ArrayList<Gesture>> name2gestures =
new HashMap<String, ArrayList<Gesture>>();
private Classifier mClassifier;
public GestureLib(String path) {
datapath = path;
mClassifier = new NearestNeighbor();
}
public Classifier getClassifier() {
return mClassifier;
}
/**
* get all the labels in the library
* @return a set of strings
*/
public Set<String> getLabels() {
return name2gestures.keySet();
}
public ArrayList<Prediction> recognize(Gesture gesture) {
Instance instance = Instance.createInstance(gesture, null);
return mClassifier.classify(instance);
}
public void addGesture(String name, Gesture gesture) {
Log.v(LOGTAG, "add an example for gesture: " + name);
ArrayList<Gesture> gestures = name2gestures.get(name);
if (gestures == null) {
gestures = new ArrayList<Gesture>();
name2gestures.put(name, gestures);
}
gestures.add(gesture);
mClassifier.addInstance(
Instance.createInstance(gesture, name));
}
public void removeGesture(String name, Gesture gesture) {
ArrayList<Gesture> gestures = name2gestures.get(name);
if (gestures == null) {
return;
}
gestures.remove(gesture);
// if there are no more samples, remove the entry automatically
if (gestures.isEmpty()) {
name2gestures.remove(name);
}
mClassifier.removeInstance(gesture.getID());
}
public ArrayList<Gesture> getGestures(String label) {
ArrayList<Gesture> gestures = name2gestures.get(label);
if (gestures != null)
return (ArrayList<Gesture>)gestures.clone();
else
return null;
}
public void load() {
String filename = datapath
+ File.separator + "gestures.xml";
File f = new File(filename);
if (f.exists()) {
try {
loadInk(filename, null);
}
catch (SAXException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
public void save() {
try {
compactSave();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void compactSave() throws IOException {
File f = new File(datapath);
if (f.exists() == false) {
f.mkdirs();
}
String filename = datapath + File.separator + "gestures.xml";
Log.v(LOGTAG, "save to " + filename);
BufferedOutputStream fos = new BufferedOutputStream(
new FileOutputStream(filename));
PrintWriter writer = new PrintWriter(fos);
XmlSerializer serializer = Xml.newSerializer();
serializer.setOutput(writer);
serializer.startDocument(Encoding.ISO_8859_1.name(), null);
serializer.startTag(namespace, "gestures");
Iterator<String> it = name2gestures.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
ArrayList<Gesture> gestures = name2gestures.get(key);
saveGestures(serializer, key, gestures);
}
serializer.endTag(namespace, "gestures");
serializer.endDocument();
serializer.flush();
writer.close();
fos.close();
}
private static void saveGestures(XmlSerializer serializer,
String name, ArrayList<Gesture> strokes) throws IOException {
serializer.startTag(namespace, "gesture");
serializer.startTag(namespace, "name");
serializer.text(name);
serializer.endTag(namespace, "name");
Iterator<Gesture> it = strokes.iterator();
while (it.hasNext()) {
Gesture stk = it.next();
stk.toXML(namespace, serializer);
}
serializer.endTag(namespace, "gesture");
}
private void loadInk(String filename, String label) throws SAXException, IOException {
Log.v(LOGTAG, "load from " + filename);
BufferedInputStream in = new BufferedInputStream(
new FileInputStream(filename));
Xml.parse(in, Encoding.ISO_8859_1, new CompactInkHandler());
in.close();
}
class CompactInkHandler implements ContentHandler {
Gesture currentGesture = null;
StringBuffer buffer = null;
String gestureName;
ArrayList<Gesture> gestures;
CompactInkHandler() {
}
// Receive notification of character data.
public void characters(char[] ch, int start, int length) {
buffer.append(ch, start, length);
}
//Receive notification of the end of a document.
public void endDocument() {
}
// Receive notification of the end of an element.
public void endElement(String uri, String localName, String qName) {
if (localName.equals("gesture")) {
name2gestures.put(gestureName, gestures);
gestures = null;
} else if (localName.equals("name")) {
gestureName = buffer.toString();
} else if (localName.equals("stroke")) {
StringTokenizer tokenizer = new StringTokenizer(buffer.toString(), ",");
while (tokenizer.hasMoreTokens()) {
String str = tokenizer.nextToken();
float x = Float.parseFloat(str);
str = tokenizer.nextToken();
float y = Float.parseFloat(str);
try
{
currentGesture.addPoint(x, y);
}
catch(Exception ex) {
ex.printStackTrace();
}
}
gestures.add(currentGesture);
mClassifier.addInstance(
Instance.createInstance(currentGesture, gestureName));
currentGesture = null;
}
}
// End the scope of a prefix-URI mapping.
public void endPrefixMapping(String prefix) {
}
//Receive notification of ignorable whitespace in element content.
public void ignorableWhitespace(char[] ch, int start, int length) {
}
//Receive notification of a processing instruction.
public void processingInstruction(String target, String data) {
}
// Receive an object for locating the origin of SAX document events.
public void setDocumentLocator(Locator locator) {
}
// Receive notification of a skipped entity.
public void skippedEntity(String name) {
}
// Receive notification of the beginning of a document.
public void startDocument() {
}
// Receive notification of the beginning of an element.
public void startElement(String uri, String localName, String qName, Attributes atts) {
if (localName.equals("gesture")) {
gestures = new ArrayList<Gesture>();
} else if (localName.equals("name")) {
buffer = new StringBuffer();
} else if (localName.equals("stroke")) {
currentGesture = new Gesture();
currentGesture.setTimestamp(Long.parseLong(atts.getValue(namespace, "timestamp")));
currentGesture.setColor(Integer.parseInt(atts.getValue(namespace, "color")));
currentGesture.setStrokeWidth(Float.parseFloat(atts.getValue(namespace, "width")));
buffer = new StringBuffer();
}
}
public void startPrefixMapping(String prefix, String uri) {
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2008-2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture;
import android.view.MotionEvent;
public interface GestureListener {
public void onStartGesture(GesturePad pad, MotionEvent event);
public void onGesture(GesturePad pad, MotionEvent event);
public void onFinishGesture(GesturePad pad, MotionEvent event);
}

View File

@@ -0,0 +1,371 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.util.ArrayList;
import java.util.Iterator;
/**
* A view for rendering and processing gestures
*/
public class GesturePad extends View {
public static final float TOUCH_TOLERANCE = 4;
public static final int default_foreground = Color.argb(255, 255, 255, 0);
private int background = Color.argb(0, 0, 0, 0);
private int foreground = default_foreground;
private int uncertain_foreground = Color.argb(55, 255, 255, 0);
private Bitmap mBitmap;
private Canvas mCanvas;
private Path mPath;
private Paint mBitmapPaint;
private Paint mPaint;
private Paint mDebugPaint;
private float mX, mY;
private boolean mEnableInput = true;
private boolean mEnableRendering = true;
private boolean mCacheGesture = true;
private Gesture mCurrentGesture = null;
ArrayList<GestureListener> mGestureListeners = new ArrayList<GestureListener>();
private boolean mShouldFadingOut = true;
private boolean mIsFadingOut = false;
private float mFadingAlpha = 1;
private boolean reconstruct = false;
private ArrayList<Path> debug = new ArrayList<Path>();
private Handler mHandler = new Handler();
private Runnable mFadingOut = new Runnable() {
public void run() {
mFadingAlpha -= 0.03f;
if (mFadingAlpha <= 0) {
mIsFadingOut = false;
mPath.reset();
} else {
mHandler.postDelayed(this, 100);
}
invalidate();
}
};
public GesturePad(Context context) {
super(context);
init();
}
public GesturePad(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public boolean isEnableRendering() {
return this.mEnableRendering;
}
public Gesture getCurrentGesture() {
return mCurrentGesture;
}
public Paint getPaint() {
return mPaint;
}
public void setColor(int c) {
this.foreground = c;
}
public void setFadingAlpha(float f) {
mFadingAlpha = f;
}
public void setCurrentGesture(Gesture stk) {
this.mCurrentGesture = stk;
reconstruct = true;
}
private void init() {
mDebugPaint = new Paint();
mDebugPaint.setColor(Color.WHITE);
mDebugPaint.setStrokeWidth(4);
mDebugPaint.setAntiAlias(true);
mDebugPaint.setStyle(Paint.Style.STROKE);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(foreground);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(12);
mBitmapPaint = new Paint(Paint.DITHER_FLAG);
mPath = new Path();
reconstruct = false;
}
public void cacheGesture(boolean b) {
mCacheGesture = b;
}
public void enableRendering(boolean b) {
mEnableRendering = b;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// TODO Auto-generated method stub
super.onSizeChanged(w, h, oldw, oldh);
if (w <=0 || h <=0)
return;
int width = w>oldw? w : oldw;
int height = h>oldh? h : oldh;
Bitmap newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(newBitmap);
if (mBitmap != null) {
mCanvas.drawColor(background);
mCanvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
mCanvas.drawPath(mPath, mPaint);
}
mBitmap = newBitmap;
}
public void addGestureListener(GestureListener l) {
this.mGestureListeners.add(l);
}
public void removeGestureListener(GestureListener l) {
this.mGestureListeners.remove(l);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(background);
if (mCacheGesture)
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
if (mIsFadingOut) {
int color = foreground;
int alpha = (int)(Color.alpha(color) * mFadingAlpha);
mPaint.setColor(Color.argb(alpha,
Color.red(color),
Color.green(color),
Color.blue(color)));
} else if (mEnableRendering == false) {
mPaint.setColor(uncertain_foreground);
} else {
mPaint.setColor(foreground);
}
if (reconstruct) {
if (this.mCurrentGesture != null) {
float xedge = 30;
float yedge = 30;
float w = this.getWidth() - 2 * xedge;
float h = this.getHeight() - 2 * yedge;
float sx = w / this.mCurrentGesture.getBBX().width();
float sy = h / mCurrentGesture.getBBX().height();
float scale = sx>sy?sy:sx;
convertFromStroke(mCurrentGesture);
Matrix matrix = new Matrix();
matrix.preTranslate(-mCurrentGesture.getBBX().centerX(), -mCurrentGesture.getBBX().centerY());
matrix.postScale(scale, scale);
matrix.postTranslate(this.getWidth()/2, this.getHeight()/2);
this.mPath.transform(matrix);
} else {
mPath.reset();
}
reconstruct = false;
}
canvas.drawPath(mPath, mPaint);
Iterator<Path> it = debug.iterator();
while (it.hasNext()) {
Path path = it.next();
canvas.drawPath(path, mDebugPaint);
}
}
public void clearDebugPath() {
debug.clear();
}
public void addDebugPath(Path path) {
debug.add(path);
}
public void addDebugPath(ArrayList<Path> paths) {
debug.addAll(paths);
}
public void clear() {
mPath = new Path();
this.mCurrentGesture = null;
mCanvas.drawColor(background);
this.invalidate();
}
private void convertFromStroke(Gesture stk) {
mPath = null;
Iterator it = stk.getPoints().iterator();
while (it.hasNext()) {
PointF p = (PointF) it.next();
if (mPath == null) {
mPath = new Path();
mPath.moveTo(p.x, p.y);
mX = p.x;
mY = p.y;
} else {
float dx = Math.abs(p.x - mX);
float dy = Math.abs(p.y - mY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
mPath.quadTo(mX, mY, (p.x + mX)/2, (p.y + mY)/2);
mX = p.x;
mY = p.y;
}
}
}
mPath.lineTo(mX, mY);
}
public void setEnableInput(boolean b) {
mEnableInput = b;
}
public boolean isEnableInput() {
return mEnableInput;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(mEnableInput == false)
return true;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(event);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(event);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up(event);
invalidate();
break;
}
return true;
}
private void touch_start(MotionEvent event) {
mIsFadingOut = false;
mHandler.removeCallbacks(mFadingOut);
float x = event.getX();
float y = event.getY();
mCurrentGesture = new Gesture();
mCurrentGesture.addPoint(x, y);
mPath.reset();
mPath.moveTo(x, y);
mX = x;
mY = y;
Iterator<GestureListener> it = mGestureListeners.iterator();
while (it.hasNext()) {
it.next().onStartGesture(this, event);
}
}
private void touch_move(MotionEvent event) {
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) {
mPath.quadTo(mX, mY, (x + mX)/2, (y + mY)/2);
mX = x;
mY = y;
}
mCurrentGesture.addPoint(x, y);
Iterator<GestureListener> it = mGestureListeners.iterator();
while (it.hasNext()) {
it.next().onGesture(this, event);
}
}
public void setFadingOut(boolean b) {
mShouldFadingOut = b;
mIsFadingOut = false;
}
public boolean shouldFadingOut() {
return mShouldFadingOut;
}
private void touch_up(MotionEvent event) {
mPath.lineTo(mX, mY);
if (mCacheGesture)
mCanvas.drawPath(mPath, mPaint);
// kill this so we don't double draw
if (shouldFadingOut()) {
mFadingAlpha = 1;
mIsFadingOut = true;
mHandler.removeCallbacks(mFadingOut);
mHandler.postDelayed(mFadingOut, 100);
}
Iterator<GestureListener> it = mGestureListeners.iterator();
while (it.hasNext()) {
it.next().onFinishGesture(this, event);
}
}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture.example;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.AdapterView.OnItemSelectedListener;
import com.android.gesture.Gesture;
import com.android.gesture.GestureLib;
import com.android.gesture.GestureListener;
import com.android.gesture.GesturePad;
import com.android.gesture.R;
import com.android.gesture.recognizer.Prediction;
import java.util.ArrayList;
/**
* The demo shows how to construct a gesture-based user interface on Android.
*/
public class GestureEntryDemo extends Activity {
private static final int DIALOG_NEW_ENTRY = 1;
private static final int NEW_ID = Menu.FIRST;
private static final int VIEW_ID = Menu.FIRST + 1;
GesturePad mView;
Spinner mResult;
GestureLib mRecognizer;
boolean mChangedByRecognizer = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.demo);
// init the recognizer
mRecognizer = new GestureLib("/sdcard/gestureentry");
mRecognizer.load();
// create the spinner for showing the recognition results
// the spinner also allows a user to correct a prediction
mResult = (Spinner) findViewById(R.id.spinner);
mResult.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// TODO Auto-generated method stub
// correct the recognition result by adding the new example
if (mChangedByRecognizer == false) {
mRecognizer.addGesture(parent.getSelectedItem().toString(),
mView.getCurrentGesture());
} else {
mChangedByRecognizer = false;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// TODO Auto-generated method stub
}
});
// create the area for drawing a gesture
mView = (GesturePad)this.findViewById(R.id.drawingpad);
mView.cacheGesture(false);
mView.setFadingOut(false);
mView.addGestureListener(new GestureListener() {
@Override
public void onFinishGesture(GesturePad patch, MotionEvent event) {
// TODO Auto-generated method stub
recognize(patch.getCurrentGesture());
}
@Override
public void onGesture(GesturePad patch, MotionEvent event) {
// TODO Auto-generated method stub
}
@Override
public void onStartGesture(GesturePad patch, MotionEvent event) {
// TODO Auto-generated method stub
}
});
if (savedInstanceState != null) {
mView.setCurrentGesture(
(Gesture)savedInstanceState.getParcelable("gesture"));
}
}
@Override
protected Dialog onCreateDialog(int id) {
// create the dialog for adding a new entry
LayoutInflater factory = LayoutInflater.from(this);
final View textEntryView =
factory.inflate(R.layout.newgesture_dialog, null);
return new AlertDialog.Builder(GestureEntryDemo.this)
.setTitle(R.string.newgesture_text_entry)
.setView(textEntryView)
.setPositiveButton(R.string.newgesture_dialog_ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
/* User clicked OK so do some stuff */
EditText edittext =
(EditText)((AlertDialog)dialog).findViewById(R.id.gesturename_edit);
String text = edittext.getText().toString().trim();
if (text.length() > 0) {
mRecognizer.addGesture(text, mView.getCurrentGesture());
}
}
})
.setNegativeButton(R.string.newgesture_dialog_cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
/* User clicked cancel so do some stuff */
}
})
.create();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// TODO Auto-generated method stub
super.onCreateOptionsMenu(menu);
menu.add(0, NEW_ID, 0, R.string.newgesture)
.setShortcut('0', 'n')
.setIcon(android.R.drawable.ic_menu_add);
menu.add(0, VIEW_ID, 0, R.string.viewgesture)
.setShortcut('1', 'v')
.setIcon(android.R.drawable.ic_menu_view);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle all of the possible menu actions.
switch (item.getItemId()) {
case NEW_ID:
// if there has been a gesture on the canvas
if (mView.getCurrentGesture() != null) {
showDialog(DIALOG_NEW_ENTRY);
}
break;
case VIEW_ID:
startActivityForResult(
new Intent(this, GestureLibViewer.class), VIEW_ID);
break;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
mRecognizer.load();
mView.clear();
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
mRecognizer.save();
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
// TODO Auto-generated method stub
super.onPrepareDialog(id, dialog);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
outState.putParcelable("gesture", mView.getCurrentGesture());
mRecognizer.save();
}
public void recognize(Gesture ink) {
mChangedByRecognizer = true;
ArrayList<Prediction> predictions = mRecognizer.recognize(ink);
ArrayAdapter adapter = new ArrayAdapter(this,
android.R.layout.simple_spinner_item, predictions);
adapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
mResult.setAdapter(adapter);
}
}

View File

@@ -0,0 +1,262 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture.example;
import android.app.Activity;
import android.graphics.Matrix;
import android.graphics.Path;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.AdapterView.OnItemSelectedListener;
import com.android.gesture.Gesture;
import com.android.gesture.GestureLib;
import com.android.gesture.GesturePad;
import com.android.gesture.R;
import com.android.gesture.recognizer.Instance;
import java.util.ArrayList;
import java.util.Collections;
/**
* GestureLibViewer is for viewing existing gestures and
* removing unwanted gestures.
*/
public class GestureLibViewer extends Activity {
GesturePad mView;
Spinner mResult;
GestureLib mRecognizer;
ArrayList<Gesture> mSamples;
int mCurrentGestureIndex;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.gestureviewer);
// create the area for drawing a glyph
mView = (GesturePad)this.findViewById(R.id.drawingpad);
mView.cacheGesture(false);
mView.setFadingOut(false);
mView.setEnableInput(false);
// init the recognizer
mRecognizer = new GestureLib("/sdcard/gestureentry");
mRecognizer.load();
mResult = (Spinner) findViewById(R.id.spinner);
ArrayList<String> list = new ArrayList<String>();
list.addAll(mRecognizer.getLabels());
Collections.sort(list);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item,
list);
adapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
mResult.setAdapter(adapter);
mSamples = mRecognizer.getGestures(list.get(0));
if (mSamples.isEmpty() == false) {
mCurrentGestureIndex = 0;
Gesture gesture = mSamples.get(mCurrentGestureIndex);
mView.setCurrentGesture(gesture);
mView.clearDebugPath();
mView.addDebugPath(
toPath(mRecognizer.getClassifier().getInstance(gesture.getID())));
}
mResult.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// TODO Auto-generated method stub
mSamples = mRecognizer.getGestures(
(String)mResult.getSelectedItem());
if (mSamples.isEmpty() == false) {
mCurrentGestureIndex = 0;
Gesture gesture = mSamples.get(mCurrentGestureIndex);
mView.setCurrentGesture(gesture);
mView.clearDebugPath();
mView.addDebugPath(
toPath(mRecognizer.getClassifier().getInstance(gesture.getID())));
}
mView.invalidate();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// TODO Auto-generated method stub
}
});
Button remove = (Button)this.findViewById(R.id.remove);
remove.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (mSamples.isEmpty())
return;
String name = (String)mResult.getSelectedItem();
Gesture gesture = mSamples.get(mCurrentGestureIndex);
mRecognizer.removeGesture(name, gesture);
mSamples = mRecognizer.getGestures(name);
if (mSamples == null) {
// delete the entire entry
mCurrentGestureIndex = 0;
ArrayList<String> list = new ArrayList<String>();
list.addAll(mRecognizer.getLabels());
Collections.sort(list);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
GestureLibViewer.this,
android.R.layout.simple_spinner_item,
list);
adapter.setDropDownViewResource(
android.R.layout.simple_spinner_dropdown_item);
mResult.setAdapter(adapter);
} else {
if (mCurrentGestureIndex > mSamples.size()-1) {
mCurrentGestureIndex--;
}
gesture = mSamples.get(mCurrentGestureIndex);
mView.setCurrentGesture(gesture);
mView.clearDebugPath();
mView.addDebugPath(
toPath(mRecognizer.getClassifier().getInstance(gesture.getID())));
mView.invalidate();
}
}
});
Button next = (Button)this.findViewById(R.id.next);
next.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (mCurrentGestureIndex >= mSamples.size()-1)
return;
mCurrentGestureIndex++;
Gesture gesture = mSamples.get(mCurrentGestureIndex);
mView.setCurrentGesture(gesture);
mView.clearDebugPath();
mView.addDebugPath(
toPath(mRecognizer.getClassifier().getInstance(gesture.getID())));
mView.invalidate();
}
});
Button previous = (Button)this.findViewById(R.id.previous);
previous.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (mCurrentGestureIndex >= 1 &&
mSamples.isEmpty() == false) {
mCurrentGestureIndex--;
Gesture gesture = mSamples.get(mCurrentGestureIndex);
mView.setCurrentGesture(gesture);
mView.clearDebugPath();
mView.addDebugPath(
toPath(mRecognizer.getClassifier().getInstance(gesture.getID())));
mView.invalidate();
}
}
});
}
public static ArrayList<Path> toPath(Instance instance) {
ArrayList<Path> paths = new ArrayList();
Path path = null;
float minx = 0, miny = 0;
float mX = 0, mY = 0;
for (int i=0; i<instance.vector.length; i+=2) {
float x = instance.vector[i];
float y = instance.vector[i+1];
if (x < minx)
minx = x;
if (y < miny)
miny = y;
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;
}
}
}
Matrix matrix = new Matrix();
matrix.setTranslate(-minx + 10, -miny + 10);
path.transform(matrix);
paths.add(path);
path = new Path();
path.moveTo(instance.vector[0]-5, instance.vector[1]-5);
path.lineTo(instance.vector[0]-5, instance.vector[1]+5);
path.lineTo(instance.vector[0]+5, instance.vector[1]+5);
path.lineTo(instance.vector[0]+5, instance.vector[1]-5);
path.close();
path.transform(matrix);
paths.add(path);
return paths;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
if (keyCode == KeyEvent.KEYCODE_BACK) {
mRecognizer.save();
this.setResult(RESULT_OK);
finish();
return true;
}
else
return false;
}
@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
mRecognizer.save();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
mRecognizer.save();
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture.recognizer;
import java.util.ArrayList;
import java.util.HashMap;
/**
* The abstract class of Classifier
*/
public abstract class Classifier {
HashMap<Long, Instance> mInstances = new HashMap<Long, Instance>();
public void addInstance(Instance instance) {
mInstances.put(instance.id, instance);
}
public Instance getInstance(long id) {
return mInstances.get(id);
}
public void removeInstance(long id) {
mInstances.remove(id);
}
public abstract ArrayList<Prediction> classify(Instance instance);
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture.recognizer;
import android.graphics.PointF;
import com.android.gesture.Gesture;
/**
* An instance represents a sample if the label is available or a query if
* the label is null.
*/
public class Instance {
private final static float[] targetOrientations = {
0, 45, 90, 135, 180, -0, -45, -90, -135, -180
};
// the feature vector
public final float[] vector;
// the label can be null
public final String label;
// the length of the vector
public final float length;
// the id of the instance
public final long id;
Instance(long d, float[] v, String l) {
id = d;
vector = v;
label = l;
float sum = 0;
for (int i = 0; i < vector.length; i++) {
sum += vector[i] * vector[i];
}
length = (float)Math.sqrt(sum);
}
public static Instance createInstance(Gesture gesture, String label) {
float[] pts = RecognitionUtil.resample(gesture, 64);
PointF center = RecognitionUtil.computeCentroid(pts);
float inductiveOrientation = (float)Math.atan2(pts[1] - center.y,
pts[0] - center.x);
inductiveOrientation *= 180 / Math.PI;
float minDeviation = Float.MAX_VALUE;
for (int i=0; i<targetOrientations.length; i++) {
float delta = targetOrientations[i] - inductiveOrientation;
if (Math.abs(delta) < Math.abs(minDeviation)) {
minDeviation = delta;
}
}
android.graphics.Matrix m = new android.graphics.Matrix();
m.setTranslate(-center.x, -center.y);
android.graphics.Matrix rotation = new android.graphics.Matrix();
rotation.setRotate(minDeviation);
m.postConcat(rotation);
m.mapPoints(pts);
return new Instance(gesture.getID(), pts, label);
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture.recognizer;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeMap;
public class NearestNeighbor extends Classifier {
private static final String LOGTAG = "NearestNeighbor";
private static final double variance = 0.25; // std = 0.5
public ArrayList<Prediction> classify(Instance instance) {
ArrayList<Prediction> list = new ArrayList<Prediction>();
Iterator<Instance> it = mInstances.values().iterator();
Log.v(LOGTAG, mInstances.size() + " instances found");
TreeMap<String, Double> label2score = new TreeMap<String, Double>();
while (it.hasNext()) {
Instance sample = it.next();
double dis = RecognitionUtil.cosineDistance(sample, instance);
double weight = Math.exp(-dis*dis/(2 * variance));
Log.v(LOGTAG, sample.label + " = " + dis + " weight = " + weight);
Double score = label2score.get(sample.label);
if (score == null) {
score = weight;
}
else {
score += weight;
}
label2score.put(sample.label, score);
}
double sum = 0;
Iterator it2 = label2score.keySet().iterator();
while (it2.hasNext()) {
String name = (String)it2.next();
double score = label2score.get(name);
sum += score;
list.add(new Prediction(name, score));
}
it2 = list.iterator();
while (it2.hasNext()) {
Prediction name = (Prediction)it2.next();
name.score /= sum;
}
Collections.sort(list, new Comparator<Prediction>() {
@Override
public int compare(Prediction object1, Prediction object2) {
// TODO Auto-generated method stub
double score1 = object1.score;
double score2 = object2.score;
if (score1 > score2)
return -1;
else if (score1 < score2)
return 1;
else
return 0;
}
});
it2 = list.iterator();
while (it2.hasNext()) {
Prediction name = (Prediction)it2.next();
Log.v(LOGTAG, "prediction [" + name.label + " = " + name.score + "]");
}
return list;
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture.recognizer;
/**
*
* A recognition result that includes the label and its score
*/
public class Prediction {
public final String label;
public double score;
public Prediction(String l, double s) {
label = l;
score = s;
}
@Override
public String toString() {
return label;
}
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright (C) 2008-2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.gesture.recognizer;
import android.graphics.PointF;
import com.android.gesture.Gesture;
import java.util.Iterator;
/**
*
* Utilities for recognition.
*/
public class RecognitionUtil {
/**
* Re-sample a list of points to a given number
* @param stk
* @param num
* @return
*/
public static float[] resample(Gesture gesture, int num) {
final float increment = gesture.getLength()/(num - 1);
float[] newstk = new float[num*2];
float distanceSoFar = 0;
Iterator<PointF> it = gesture.getPoints().iterator();
PointF lstPoint = it.next();
int index = 0;
PointF currentPoint = null;
try
{
newstk[index] = lstPoint.x;
index++;
newstk[index] = lstPoint.y;
index++;
while (it.hasNext()) {
if (currentPoint == null)
currentPoint = it.next();
float deltaX = currentPoint.x - lstPoint.x;
float deltaY = currentPoint.y - lstPoint.y;
float distance = (float)Math.sqrt(deltaX*deltaX+deltaY*deltaY);
if (distanceSoFar+distance >= increment) {
float ratio = (increment - distanceSoFar) / distance;
float nx = lstPoint.x + ratio * deltaX;
float ny = lstPoint.y + ratio * deltaY;
newstk[index] = nx;
index++;
newstk[index] = ny;
index++;
lstPoint = new PointF(nx, ny);
distanceSoFar = 0;
}
else {
lstPoint = currentPoint;
currentPoint = null;
distanceSoFar += distance;
}
}
}
catch(Exception ex) {
ex.printStackTrace();
}
for(int i = index; i < newstk.length -1; i+=2) {
newstk[i] = lstPoint.x;
newstk[i+1] = lstPoint.y;
}
return newstk;
}
/**
* Calculate the centroid of a list of points
* @param points
* @return the centroid
*/
public static PointF computeCentroid(float[] points) {
float centerX = 0;
float centerY = 0;
for(int i=0; i<points.length; i++)
{
centerX += points[i];
i++;
centerY += points[i];
}
centerX = 2 * centerX/points.length;
centerY = 2 * centerY/points.length;
return new PointF(centerX, centerY);
}
/**
* calculate the variance-covariance matrix, treat each point as a sample
* @param points
* @return
*/
public 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;
for(int i=0; i<points.length; 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] /= (points.length/2);
array[0][1] /= (points.length/2);
array[1][0] /= (points.length/2);
array[1][1] /= (points.length/2);
return array;
}
public static float computeTotalLength(float[] points) {
float sum = 0;
for (int i=0; i<points.length - 4; 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;
}
public 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;
}
public 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;
}
public static double averageEuclidDistance(float[] stk1, float[] stk2) {
double distance = 0;
for (int i = 0; i < stk1.length; i += 2) {
distance += PointF.length(stk1[i] - stk2[i], stk1[i+1] - stk2[i+1]);
}
return distance/stk1.length;
}
public static double squaredEuclidDistance(float[] stk1, float[] stk2) {
double squaredDistance = 0;
for (int i = 0; i < stk1.length; i++) {
float difference = stk1[i] - stk2[i];
squaredDistance += difference * difference;
}
return squaredDistance/stk1.length;
}
/**
* Calculate the cosine distance between two instances
* @param in1
* @param in2
* @return the angle between 0 and Math.PI
*/
public static double cosineDistance(Instance in1, Instance in2) {
float sum = 0;
for (int i = 0; i < in1.vector.length; i++) {
sum += in1.vector[i] * in2.vector[i];
}
return Math.acos(sum / (in1.length * in2.length));
}
}