diff options
author | Joe Onorato <joeo@android.com> | 2009-04-17 14:18:46 -0700 |
---|---|---|
committer | Joe Onorato <joeo@android.com> | 2009-04-17 14:18:46 -0700 |
commit | 6e93a3db56d6add29b43077718a4cad9ccfc047f (patch) | |
tree | bddbb122cf6414d6a8ed97a623b86be0d14c4480 /tests | |
parent | 21b5817aaa2f0a61edff8752ed85130aa8cf7def (diff) | |
download | frameworks_base-6e93a3db56d6add29b43077718a4cad9ccfc047f.zip frameworks_base-6e93a3db56d6add29b43077718a4cad9ccfc047f.tar.gz frameworks_base-6e93a3db56d6add29b43077718a4cad9ccfc047f.tar.bz2 |
Add sketch gesture demo application.
Initial checkin, there's no Android.mk yet.
Diffstat (limited to 'tests')
17 files changed, 2144 insertions, 0 deletions
diff --git a/tests/sketch/AndroidManifest.xml b/tests/sketch/AndroidManifest.xml new file mode 100755 index 0000000..c44b54e --- /dev/null +++ b/tests/sketch/AndroidManifest.xml @@ -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> diff --git a/tests/sketch/res/drawable/icon.png b/tests/sketch/res/drawable/icon.png Binary files differnew file mode 100755 index 0000000..7502484 --- /dev/null +++ b/tests/sketch/res/drawable/icon.png diff --git a/tests/sketch/res/layout/demo.xml b/tests/sketch/res/layout/demo.xml new file mode 100755 index 0000000..e516229 --- /dev/null +++ b/tests/sketch/res/layout/demo.xml @@ -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> diff --git a/tests/sketch/res/layout/gestureviewer.xml b/tests/sketch/res/layout/gestureviewer.xml new file mode 100755 index 0000000..5302d34 --- /dev/null +++ b/tests/sketch/res/layout/gestureviewer.xml @@ -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> diff --git a/tests/sketch/res/layout/newgesture_dialog.xml b/tests/sketch/res/layout/newgesture_dialog.xml new file mode 100755 index 0000000..6e45d81 --- /dev/null +++ b/tests/sketch/res/layout/newgesture_dialog.xml @@ -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> diff --git a/tests/sketch/res/values/strings.xml b/tests/sketch/res/values/strings.xml new file mode 100755 index 0000000..4c6aa20 --- /dev/null +++ b/tests/sketch/res/values/strings.xml @@ -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> diff --git a/tests/sketch/src/com/android/gesture/Gesture.java b/tests/sketch/src/com/android/gesture/Gesture.java new file mode 100755 index 0000000..29c07ad --- /dev/null +++ b/tests/sketch/src/com/android/gesture/Gesture.java @@ -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; + } +} diff --git a/tests/sketch/src/com/android/gesture/GestureLib.java b/tests/sketch/src/com/android/gesture/GestureLib.java new file mode 100755 index 0000000..d0a25f2 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/GestureLib.java @@ -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) { + } + } +} diff --git a/tests/sketch/src/com/android/gesture/GestureListener.java b/tests/sketch/src/com/android/gesture/GestureListener.java new file mode 100755 index 0000000..ebb4149 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/GestureListener.java @@ -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); +} diff --git a/tests/sketch/src/com/android/gesture/GesturePad.java b/tests/sketch/src/com/android/gesture/GesturePad.java new file mode 100755 index 0000000..45a09e6 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/GesturePad.java @@ -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); + } + } + +} diff --git a/tests/sketch/src/com/android/gesture/example/GestureEntryDemo.java b/tests/sketch/src/com/android/gesture/example/GestureEntryDemo.java new file mode 100755 index 0000000..0f8d317 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/example/GestureEntryDemo.java @@ -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); + } + +} diff --git a/tests/sketch/src/com/android/gesture/example/GestureLibViewer.java b/tests/sketch/src/com/android/gesture/example/GestureLibViewer.java new file mode 100755 index 0000000..a9893bc --- /dev/null +++ b/tests/sketch/src/com/android/gesture/example/GestureLibViewer.java @@ -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(); + } +} diff --git a/tests/sketch/src/com/android/gesture/recognizer/Classifier.java b/tests/sketch/src/com/android/gesture/recognizer/Classifier.java new file mode 100755 index 0000000..584e0a5 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/recognizer/Classifier.java @@ -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); +} diff --git a/tests/sketch/src/com/android/gesture/recognizer/Instance.java b/tests/sketch/src/com/android/gesture/recognizer/Instance.java new file mode 100755 index 0000000..2eaa1c2 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/recognizer/Instance.java @@ -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); + } +} diff --git a/tests/sketch/src/com/android/gesture/recognizer/NearestNeighbor.java b/tests/sketch/src/com/android/gesture/recognizer/NearestNeighbor.java new file mode 100755 index 0000000..2035fad --- /dev/null +++ b/tests/sketch/src/com/android/gesture/recognizer/NearestNeighbor.java @@ -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; + } +} diff --git a/tests/sketch/src/com/android/gesture/recognizer/Prediction.java b/tests/sketch/src/com/android/gesture/recognizer/Prediction.java new file mode 100755 index 0000000..c318754 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/recognizer/Prediction.java @@ -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; + } +} diff --git a/tests/sketch/src/com/android/gesture/recognizer/RecognitionUtil.java b/tests/sketch/src/com/android/gesture/recognizer/RecognitionUtil.java new file mode 100755 index 0000000..9146b95 --- /dev/null +++ b/tests/sketch/src/com/android/gesture/recognizer/RecognitionUtil.java @@ -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)); + } + +} |