1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.gesture;
18 
19 import android.graphics.Canvas;
20 import android.graphics.Paint;
21 import android.graphics.Path;
22 import android.graphics.RectF;
23 
24 import java.io.IOException;
25 import java.io.DataOutputStream;
26 import java.io.DataInputStream;
27 import java.util.ArrayList;
28 
29 /**
30  * A gesture stroke started on a touch down and ended on a touch up. A stroke
31  * consists of a sequence of timed points. One or multiple strokes form a gesture.
32  */
33 public class GestureStroke {
34     static final float TOUCH_TOLERANCE = 3;
35 
36     public final RectF boundingBox;
37 
38     public final float length;
39     public final float[] points;
40 
41     private final long[] timestamps;
42     private Path mCachedPath;
43 
44     /**
45      * A constructor that constructs a gesture stroke from a list of gesture points.
46      *
47      * @param points
48      */
GestureStroke(ArrayList<GesturePoint> points)49     public GestureStroke(ArrayList<GesturePoint> points) {
50         final int count = points.size();
51         final float[] tmpPoints = new float[count * 2];
52         final long[] times = new long[count];
53 
54         RectF bx = null;
55         float len = 0;
56         int index = 0;
57 
58         for (int i = 0; i < count; i++) {
59             final GesturePoint p = points.get(i);
60             tmpPoints[i * 2] = p.x;
61             tmpPoints[i * 2 + 1] = p.y;
62             times[index] = p.timestamp;
63 
64             if (bx == null) {
65                 bx = new RectF();
66                 bx.top = p.y;
67                 bx.left = p.x;
68                 bx.right = p.x;
69                 bx.bottom = p.y;
70                 len = 0;
71             } else {
72                 len += Math.hypot(p.x - tmpPoints[(i - 1) * 2], p.y - tmpPoints[(i -1) * 2 + 1]);
73                 bx.union(p.x, p.y);
74             }
75             index++;
76         }
77 
78         timestamps = times;
79         this.points = tmpPoints;
80         boundingBox = bx;
81         length = len;
82     }
83 
84     /**
85      * A faster constructor specially for cloning a stroke.
86      */
GestureStroke(RectF bbx, float len, float[] pts, long[] times)87     private GestureStroke(RectF bbx, float len, float[] pts, long[] times) {
88         boundingBox = new RectF(bbx.left, bbx.top, bbx.right, bbx.bottom);
89         length = len;
90         points = pts.clone();
91         timestamps = times.clone();
92     }
93 
94     @Override
clone()95     public Object clone() {
96         return new GestureStroke(boundingBox, length, points, timestamps);
97     }
98 
99     /**
100      * Draws the stroke with a given canvas and paint.
101      *
102      * @param canvas
103      */
draw(Canvas canvas, Paint paint)104     void draw(Canvas canvas, Paint paint) {
105         if (mCachedPath == null) {
106             makePath();
107         }
108 
109         canvas.drawPath(mCachedPath, paint);
110     }
111 
getPath()112     public Path getPath() {
113         if (mCachedPath == null) {
114             makePath();
115         }
116 
117         return mCachedPath;
118     }
119 
makePath()120     private void makePath() {
121         final float[] localPoints = points;
122         final int count = localPoints.length;
123 
124         Path path = null;
125 
126         float mX = 0;
127         float mY = 0;
128 
129         for (int i = 0; i < count; i += 2) {
130             float x = localPoints[i];
131             float y = localPoints[i + 1];
132             if (path == null) {
133                 path = new Path();
134                 path.moveTo(x, y);
135                 mX = x;
136                 mY = y;
137             } else {
138                 float dx = Math.abs(x - mX);
139                 float dy = Math.abs(y - mY);
140                 if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
141                     path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
142                     mX = x;
143                     mY = y;
144                 }
145             }
146         }
147 
148         mCachedPath = path;
149     }
150 
151     /**
152      * Converts the stroke to a Path of a given number of points.
153      *
154      * @param width the width of the bounding box of the target path
155      * @param height the height of the bounding box of the target path
156      * @param numSample the number of points needed
157      *
158      * @return the path
159      */
toPath(float width, float height, int numSample)160     public Path toPath(float width, float height, int numSample) {
161         final float[] pts = GestureUtils.temporalSampling(this, numSample);
162         final RectF rect = boundingBox;
163 
164         GestureUtils.translate(pts, -rect.left, -rect.top);
165 
166         float sx = width / rect.width();
167         float sy = height / rect.height();
168         float scale = sx > sy ? sy : sx;
169         GestureUtils.scale(pts, scale, scale);
170 
171         float mX = 0;
172         float mY = 0;
173 
174         Path path = null;
175 
176         final int count = pts.length;
177 
178         for (int i = 0; i < count; i += 2) {
179             float x = pts[i];
180             float y = pts[i + 1];
181             if (path == null) {
182                 path = new Path();
183                 path.moveTo(x, y);
184                 mX = x;
185                 mY = y;
186             } else {
187                 float dx = Math.abs(x - mX);
188                 float dy = Math.abs(y - mY);
189                 if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
190                     path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
191                     mX = x;
192                     mY = y;
193                 }
194             }
195         }
196 
197         return path;
198     }
199 
serialize(DataOutputStream out)200     void serialize(DataOutputStream out) throws IOException {
201         final float[] pts = points;
202         final long[] times = timestamps;
203         final int count = points.length;
204 
205         // Write number of points
206         out.writeInt(count / 2);
207 
208         for (int i = 0; i < count; i += 2) {
209             // Write X
210             out.writeFloat(pts[i]);
211             // Write Y
212             out.writeFloat(pts[i + 1]);
213             // Write timestamp
214             out.writeLong(times[i / 2]);
215         }
216     }
217 
deserialize(DataInputStream in)218     static GestureStroke deserialize(DataInputStream in) throws IOException {
219         // Number of points
220         final int count = in.readInt();
221 
222         final ArrayList<GesturePoint> points = new ArrayList<GesturePoint>(count);
223         for (int i = 0; i < count; i++) {
224             points.add(GesturePoint.deserialize(in));
225         }
226 
227         return new GestureStroke(points);
228     }
229 
230     /**
231      * Invalidates the cached path that is used to render the stroke.
232      */
clearPath()233     public void clearPath() {
234         if (mCachedPath != null) mCachedPath.rewind();
235     }
236 
237     /**
238      * Computes an oriented bounding box of the stroke.
239      *
240      * @return OrientedBoundingBox
241      */
computeOrientedBoundingBox()242     public OrientedBoundingBox computeOrientedBoundingBox() {
243         return GestureUtils.computeOrientedBoundingBox(points);
244     }
245 }
246