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
17package com.android.inputmethod.keyboard.internal;
18
19import android.graphics.Canvas;
20import android.graphics.Color;
21import android.graphics.Paint;
22import android.graphics.Path;
23import android.graphics.Rect;
24import android.os.SystemClock;
25
26import com.android.inputmethod.latin.common.Constants;
27import 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 */
37final 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
59    private static int markAsDownEvent(final int xCoord) {
60        return DOWN_EVENT_MARKER - xCoord;
61    }
62
63    private static boolean isDownEventXCoord(final int xCoordOrMark) {
64        return xCoordOrMark <= DOWN_EVENT_MARKER;
65    }
66
67    private static int getXCoordValue(final int xCoordOrMark) {
68        return isDownEventXCoord(xCoordOrMark)
69                ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark;
70    }
71
72    public void addStroke(final GestureStrokeDrawingPoints stroke, final long downTime) {
73        synchronized (mEventTimes) {
74            addStrokeLocked(stroke, downTime);
75        }
76    }
77
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     */
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     */
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     */
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
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
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