1 /*
2  * Copyright (C) 2012 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 com.android.inputmethod.keyboard.internal;
18 
19 import android.graphics.Canvas;
20 import android.graphics.Color;
21 import android.graphics.Paint;
22 import android.graphics.Path;
23 import android.graphics.Rect;
24 import android.os.SystemClock;
25 
26 import com.android.inputmethod.latin.common.Constants;
27 import com.android.inputmethod.latin.common.ResizableIntArray;
28 
29 /**
30  * This class holds drawing points to represent a gesture trail. The gesture trail may contain
31  * multiple non-contiguous gesture strokes and will be animated asynchronously from gesture input.
32  *
33  * On the other hand, {@link GestureStrokeDrawingPoints} class holds drawing points of each gesture
34  * stroke. This class holds drawing points of those gesture strokes to draw as a gesture trail.
35  * Drawing points in this class will be asynchronously removed when fading out animation goes.
36  */
37 final class GestureTrailDrawingPoints {
38     public static final boolean DEBUG_SHOW_POINTS = false;
39     public static final int POINT_TYPE_SAMPLED = 1;
40     public static final int POINT_TYPE_INTERPOLATED = 2;
41 
42     private static final int DEFAULT_CAPACITY = GestureStrokeDrawingPoints.PREVIEW_CAPACITY;
43 
44     // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}.
45     private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
46     private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
47     private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
48     private final ResizableIntArray mPointTypes = new ResizableIntArray(
49             DEBUG_SHOW_POINTS ? DEFAULT_CAPACITY : 0);
50     private int mCurrentStrokeId = -1;
51     // The wall time of the zero value in {@link #mEventTimes}
52     private long mCurrentTimeBase;
53     private int mTrailStartIndex;
54     private int mLastInterpolatedDrawIndex;
55 
56     // Use this value as imaginary zero because x-coordinates may be zero.
57     private static final int DOWN_EVENT_MARKER = -128;
58 
markAsDownEvent(final int xCoord)59     private static int markAsDownEvent(final int xCoord) {
60         return DOWN_EVENT_MARKER - xCoord;
61     }
62 
isDownEventXCoord(final int xCoordOrMark)63     private static boolean isDownEventXCoord(final int xCoordOrMark) {
64         return xCoordOrMark <= DOWN_EVENT_MARKER;
65     }
66 
getXCoordValue(final int xCoordOrMark)67     private static int getXCoordValue(final int xCoordOrMark) {
68         return isDownEventXCoord(xCoordOrMark)
69                 ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark;
70     }
71 
addStroke(final GestureStrokeDrawingPoints stroke, final long downTime)72     public void addStroke(final GestureStrokeDrawingPoints stroke, final long downTime) {
73         synchronized (mEventTimes) {
74             addStrokeLocked(stroke, downTime);
75         }
76     }
77 
addStrokeLocked(final GestureStrokeDrawingPoints stroke, final long downTime)78     private void addStrokeLocked(final GestureStrokeDrawingPoints stroke, final long downTime) {
79         final int trailSize = mEventTimes.getLength();
80         stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates, mPointTypes);
81         if (mEventTimes.getLength() == trailSize) {
82             return;
83         }
84         final int[] eventTimes = mEventTimes.getPrimitiveArray();
85         final int strokeId = stroke.getGestureStrokeId();
86         // Because interpolation algorithm in {@link GestureStrokeDrawingPoints} can't determine
87         // the interpolated points in the last segment of gesture stroke, it may need recalculation
88         // of interpolation when new segments are added to the stroke.
89         // {@link #mLastInterpolatedDrawIndex} holds the start index of the last segment. It may
90         // be updated by the interpolation
91         // {@link GestureStrokeDrawingPoints#interpolatePreviewStroke}
92         // or by animation {@link #drawGestureTrail(Canvas,Paint,Rect,GestureTrailDrawingParams)}
93         // below.
94         final int lastInterpolatedIndex = (strokeId == mCurrentStrokeId)
95                 ? mLastInterpolatedDrawIndex : trailSize;
96         mLastInterpolatedDrawIndex = stroke.interpolateStrokeAndReturnStartIndexOfLastSegment(
97                 lastInterpolatedIndex, mEventTimes, mXCoordinates, mYCoordinates, mPointTypes);
98         if (strokeId != mCurrentStrokeId) {
99             final int elapsedTime = (int)(downTime - mCurrentTimeBase);
100             for (int i = mTrailStartIndex; i < trailSize; i++) {
101                 // Decay the previous strokes' event times.
102                 eventTimes[i] -= elapsedTime;
103             }
104             final int[] xCoords = mXCoordinates.getPrimitiveArray();
105             final int downIndex = trailSize;
106             xCoords[downIndex] = markAsDownEvent(xCoords[downIndex]);
107             mCurrentTimeBase = downTime - eventTimes[downIndex];
108             mCurrentStrokeId = strokeId;
109         }
110     }
111 
112     /**
113      * Calculate the alpha of a gesture trail.
114      * A gesture trail starts from fully opaque. After mFadeStartDelay has been passed, the alpha
115      * of a trail reduces in proportion to the elapsed time. Then after mFadeDuration has been
116      * passed, a trail becomes fully transparent.
117      *
118      * @param elapsedTime the elapsed time since a trail has been made.
119      * @param params gesture trail display parameters
120      * @return the width of a gesture trail
121      */
getAlpha(final int elapsedTime, final GestureTrailDrawingParams params)122     private static int getAlpha(final int elapsedTime, final GestureTrailDrawingParams params) {
123         if (elapsedTime < params.mFadeoutStartDelay) {
124             return Constants.Color.ALPHA_OPAQUE;
125         }
126         final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE
127                 * (elapsedTime - params.mFadeoutStartDelay)
128                 / params.mFadeoutDuration;
129         return Constants.Color.ALPHA_OPAQUE - decreasingAlpha;
130     }
131 
132     /**
133      * Calculate the width of a gesture trail.
134      * A gesture trail starts from the width of mTrailStartWidth and reduces its width in proportion
135      * to the elapsed time. After mTrailEndWidth has been passed, the width becomes mTraiLEndWidth.
136      *
137      * @param elapsedTime the elapsed time since a trail has been made.
138      * @param params gesture trail display parameters
139      * @return the width of a gesture trail
140      */
getWidth(final int elapsedTime, final GestureTrailDrawingParams params)141     private static float getWidth(final int elapsedTime, final GestureTrailDrawingParams params) {
142         final float deltaWidth = params.mTrailStartWidth - params.mTrailEndWidth;
143         return params.mTrailStartWidth - (deltaWidth * elapsedTime) / params.mTrailLingerDuration;
144     }
145 
146     private final RoundedLine mRoundedLine = new RoundedLine();
147     private final Rect mRoundedLineBounds = new Rect();
148 
149     /**
150      * Draw gesture trail
151      * @param canvas The canvas to draw the gesture trail
152      * @param paint The paint object to be used to draw the gesture trail
153      * @param outBoundsRect the bounding box of this gesture trail drawing
154      * @param params The drawing parameters of gesture trail
155      * @return true if some gesture trails remain to be drawn
156      */
drawGestureTrail(final Canvas canvas, final Paint paint, final Rect outBoundsRect, final GestureTrailDrawingParams params)157     public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
158             final Rect outBoundsRect, final GestureTrailDrawingParams params) {
159         synchronized (mEventTimes) {
160             return drawGestureTrailLocked(canvas, paint, outBoundsRect, params);
161         }
162     }
163 
drawGestureTrailLocked(final Canvas canvas, final Paint paint, final Rect outBoundsRect, final GestureTrailDrawingParams params)164     private boolean drawGestureTrailLocked(final Canvas canvas, final Paint paint,
165             final Rect outBoundsRect, final GestureTrailDrawingParams params) {
166         // Initialize bounds rectangle.
167         outBoundsRect.setEmpty();
168         final int trailSize = mEventTimes.getLength();
169         if (trailSize == 0) {
170             return false;
171         }
172 
173         final int[] eventTimes = mEventTimes.getPrimitiveArray();
174         final int[] xCoords = mXCoordinates.getPrimitiveArray();
175         final int[] yCoords = mYCoordinates.getPrimitiveArray();
176         final int[] pointTypes = mPointTypes.getPrimitiveArray();
177         final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentTimeBase);
178         int startIndex;
179         for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) {
180             final int elapsedTime = sinceDown - eventTimes[startIndex];
181             // Skip too old trail points.
182             if (elapsedTime < params.mTrailLingerDuration) {
183                 break;
184             }
185         }
186         mTrailStartIndex = startIndex;
187 
188         if (startIndex < trailSize) {
189             paint.setColor(params.mTrailColor);
190             paint.setStyle(Paint.Style.FILL);
191             final RoundedLine roundedLine = mRoundedLine;
192             int p1x = getXCoordValue(xCoords[startIndex]);
193             int p1y = yCoords[startIndex];
194             final int lastTime = sinceDown - eventTimes[startIndex];
195             float r1 = getWidth(lastTime, params) / 2.0f;
196             for (int i = startIndex + 1; i < trailSize; i++) {
197                 final int elapsedTime = sinceDown - eventTimes[i];
198                 final int p2x = getXCoordValue(xCoords[i]);
199                 final int p2y = yCoords[i];
200                 final float r2 = getWidth(elapsedTime, params) / 2.0f;
201                 // Draw trail line only when the current point isn't a down point.
202                 if (!isDownEventXCoord(xCoords[i])) {
203                     final float body1 = r1 * params.mTrailBodyRatio;
204                     final float body2 = r2 * params.mTrailBodyRatio;
205                     final Path path = roundedLine.makePath(p1x, p1y, body1, p2x, p2y, body2);
206                     if (!path.isEmpty()) {
207                         roundedLine.getBounds(mRoundedLineBounds);
208                         if (params.mTrailShadowEnabled) {
209                             final float shadow2 = r2 * params.mTrailShadowRatio;
210                             paint.setShadowLayer(shadow2, 0.0f, 0.0f, params.mTrailColor);
211                             final int shadowInset = -(int)Math.ceil(shadow2);
212                             mRoundedLineBounds.inset(shadowInset, shadowInset);
213                         }
214                         // Take union for the bounds.
215                         outBoundsRect.union(mRoundedLineBounds);
216                         final int alpha = getAlpha(elapsedTime, params);
217                         paint.setAlpha(alpha);
218                         canvas.drawPath(path, paint);
219                     }
220                 }
221                 p1x = p2x;
222                 p1y = p2y;
223                 r1 = r2;
224             }
225             if (DEBUG_SHOW_POINTS) {
226                 debugDrawPoints(canvas, startIndex, trailSize, paint);
227             }
228         }
229 
230         final int newSize = trailSize - startIndex;
231         if (newSize < startIndex) {
232             mTrailStartIndex = 0;
233             if (newSize > 0) {
234                 System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
235                 System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
236                 System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
237                 if (DEBUG_SHOW_POINTS) {
238                     System.arraycopy(pointTypes, startIndex, pointTypes, 0, newSize);
239                 }
240             }
241             mEventTimes.setLength(newSize);
242             mXCoordinates.setLength(newSize);
243             mYCoordinates.setLength(newSize);
244             if (DEBUG_SHOW_POINTS) {
245                 mPointTypes.setLength(newSize);
246             }
247             // The start index of the last segment of the stroke
248             // {@link mLastInterpolatedDrawIndex} should also be updated because all array
249             // elements have just been shifted for compaction or been zeroed.
250             mLastInterpolatedDrawIndex = Math.max(mLastInterpolatedDrawIndex - startIndex, 0);
251         }
252         return newSize > 0;
253     }
254 
debugDrawPoints(final Canvas canvas, final int startIndex, final int endIndex, final Paint paint)255     private void debugDrawPoints(final Canvas canvas, final int startIndex, final int endIndex,
256             final Paint paint) {
257         final int[] xCoords = mXCoordinates.getPrimitiveArray();
258         final int[] yCoords = mYCoordinates.getPrimitiveArray();
259         final int[] pointTypes = mPointTypes.getPrimitiveArray();
260         // {@link Paint} that is zero width stroke and anti alias off draws exactly 1 pixel.
261         paint.setAntiAlias(false);
262         paint.setStrokeWidth(0);
263         for (int i = startIndex; i < endIndex; i++) {
264             final int pointType = pointTypes[i];
265             if (pointType == POINT_TYPE_INTERPOLATED) {
266                 paint.setColor(Color.RED);
267             } else if (pointType == POINT_TYPE_SAMPLED) {
268                 paint.setColor(0xFFA000FF);
269             } else {
270                 paint.setColor(Color.GREEN);
271             }
272             canvas.drawPoint(getXCoordValue(xCoords[i]), yCoords[i], paint);
273         }
274         paint.setAntiAlias(true);
275     }
276 }
277