1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package com.android.inputmethod.keyboard.internal;
16
17import android.content.res.TypedArray;
18import android.graphics.Canvas;
19import android.graphics.Paint;
20import android.graphics.Path;
21import android.graphics.Rect;
22import android.graphics.RectF;
23import android.os.SystemClock;
24
25import com.android.inputmethod.latin.Constants;
26import com.android.inputmethod.latin.R;
27import com.android.inputmethod.latin.ResizableIntArray;
28
29final class GesturePreviewTrail {
30    private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
31
32    private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
33    private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
34    private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
35    private int mCurrentStrokeId = -1;
36    // The wall time of the zero value in {@link #mEventTimes}
37    private long mCurrentTimeBase;
38    private int mTrailStartIndex;
39
40    static final class Params {
41        public final int mTrailColor;
42        public final float mTrailStartWidth;
43        public final float mTrailEndWidth;
44        public final int mFadeoutStartDelay;
45        public final int mFadeoutDuration;
46        public final int mUpdateInterval;
47
48        public final int mTrailLingerDuration;
49
50        public Params(final TypedArray keyboardViewAttr) {
51            mTrailColor = keyboardViewAttr.getColor(
52                    R.styleable.KeyboardView_gesturePreviewTrailColor, 0);
53            mTrailStartWidth = keyboardViewAttr.getDimension(
54                    R.styleable.KeyboardView_gesturePreviewTrailStartWidth, 0.0f);
55            mTrailEndWidth = keyboardViewAttr.getDimension(
56                    R.styleable.KeyboardView_gesturePreviewTrailEndWidth, 0.0f);
57            mFadeoutStartDelay = keyboardViewAttr.getInt(
58                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
59            mFadeoutDuration = keyboardViewAttr.getInt(
60                    R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0);
61            mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration;
62            mUpdateInterval = keyboardViewAttr.getInt(
63                    R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0);
64        }
65    }
66
67    // Use this value as imaginary zero because x-coordinates may be zero.
68    private static final int DOWN_EVENT_MARKER = -128;
69
70    private static int markAsDownEvent(final int xCoord) {
71        return DOWN_EVENT_MARKER - xCoord;
72    }
73
74    private static boolean isDownEventXCoord(final int xCoordOrMark) {
75        return xCoordOrMark <= DOWN_EVENT_MARKER;
76    }
77
78    private static int getXCoordValue(final int xCoordOrMark) {
79        return isDownEventXCoord(xCoordOrMark)
80                ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark;
81    }
82
83    public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) {
84        final int trailSize = mEventTimes.getLength();
85        stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
86        if (mEventTimes.getLength() == trailSize) {
87            return;
88        }
89        final int[] eventTimes = mEventTimes.getPrimitiveArray();
90        final int strokeId = stroke.getGestureStrokeId();
91        if (strokeId != mCurrentStrokeId) {
92            final int elapsedTime = (int)(downTime - mCurrentTimeBase);
93            for (int i = mTrailStartIndex; i < trailSize; i++) {
94                // Decay the previous strokes' event times.
95                eventTimes[i] -= elapsedTime;
96            }
97            final int[] xCoords = mXCoordinates.getPrimitiveArray();
98            final int downIndex = trailSize;
99            xCoords[downIndex] = markAsDownEvent(xCoords[downIndex]);
100            mCurrentTimeBase = downTime - eventTimes[downIndex];
101            mCurrentStrokeId = strokeId;
102        }
103    }
104
105    private static int getAlpha(final int elapsedTime, final Params params) {
106        if (elapsedTime < params.mFadeoutStartDelay) {
107            return Constants.Color.ALPHA_OPAQUE;
108        }
109        final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE
110                * (elapsedTime - params.mFadeoutStartDelay)
111                / params.mFadeoutDuration;
112        return Constants.Color.ALPHA_OPAQUE - decreasingAlpha;
113    }
114
115    private static float getWidth(final int elapsedTime, final Params params) {
116        return Math.max((params.mTrailLingerDuration - elapsedTime)
117                * (params.mTrailStartWidth - params.mTrailEndWidth)
118                / params.mTrailLingerDuration, 0.0f);
119    }
120
121    static final class WorkingSet {
122        // Input
123        // Previous point (P1) coordinates and trail radius.
124        public float p1x, p1y;
125        public float r1;
126        // Current point (P2) coordinates and trail radius.
127        public float p2x, p2y;
128        public float r2;
129
130        // Output
131        // Closing point of arc at P1.
132        public float p1ax, p1ay;
133        // Opening point of arc at P1.
134        public float p1bx, p1by;
135        // Opening point of arc at P2.
136        public float p2ax, p2ay;
137        // Closing point of arc at P2.
138        public float p2bx, p2by;
139        // Start angle of the trail arcs.
140        public float aa;
141        // Sweep angle of the trail arc at P1.
142        public float a1;
143        public RectF arc1 = new RectF();
144        // Sweep angle of the trail arc at P2.
145        public float a2;
146        public RectF arc2 = new RectF();
147    }
148
149    private static final float RIGHT_ANGLE = (float)(Math.PI / 2.0d);
150    private static final float RADIAN_TO_DEGREE = (float)(180.0d / Math.PI);
151
152    private static boolean calculatePathPoints(final WorkingSet w) {
153        final float dx = w.p2x - w.p1x;
154        final float dy = w.p2y - w.p1y;
155        // Distance of the points.
156        final double l = Math.hypot(dx, dy);
157        if (Double.compare(0.0d, l) == 0) {
158            return false;
159        }
160        // Angle of the line p1-p2
161        final float a = (float)Math.atan2(dy, dx);
162        // Difference of trail cap radius.
163        final float dr = w.r2 - w.r1;
164        // Variation of angle at trail cap.
165        final float ar = (float)Math.asin(dr / l);
166        // The start angle of trail cap arc at P1.
167        final float aa = a - (RIGHT_ANGLE + ar);
168        // The end angle of trail cap arc at P2.
169        final float ab = a + (RIGHT_ANGLE + ar);
170        final float cosa = (float)Math.cos(aa);
171        final float sina = (float)Math.sin(aa);
172        final float cosb = (float)Math.cos(ab);
173        final float sinb = (float)Math.sin(ab);
174        w.p1ax = w.p1x + w.r1 * cosa;
175        w.p1ay = w.p1y + w.r1 * sina;
176        w.p1bx = w.p1x + w.r1 * cosb;
177        w.p1by = w.p1y + w.r1 * sinb;
178        w.p2ax = w.p2x + w.r2 * cosa;
179        w.p2ay = w.p2y + w.r2 * sina;
180        w.p2bx = w.p2x + w.r2 * cosb;
181        w.p2by = w.p2y + w.r2 * sinb;
182        w.aa = aa * RADIAN_TO_DEGREE;
183        final float ar2degree = ar * 2.0f * RADIAN_TO_DEGREE;
184        w.a1 = -180.0f + ar2degree;
185        w.a2 = 180.0f + ar2degree;
186        w.arc1.set(w.p1x, w.p1y, w.p1x, w.p1y);
187        w.arc1.inset(-w.r1, -w.r1);
188        w.arc2.set(w.p2x, w.p2y, w.p2x, w.p2y);
189        w.arc2.inset(-w.r2, -w.r2);
190        return true;
191    }
192
193    private static void createPath(final Path path, final WorkingSet w) {
194        path.rewind();
195        // Trail cap at P1.
196        path.moveTo(w.p1x, w.p1y);
197        path.arcTo(w.arc1, w.aa, w.a1);
198        // Trail cap at P2.
199        path.moveTo(w.p2x, w.p2y);
200        path.arcTo(w.arc2, w.aa, w.a2);
201        // Two trapezoids connecting P1 and P2.
202        path.moveTo(w.p1ax, w.p1ay);
203        path.lineTo(w.p1x, w.p1y);
204        path.lineTo(w.p1bx, w.p1by);
205        path.lineTo(w.p2bx, w.p2by);
206        path.lineTo(w.p2x, w.p2y);
207        path.lineTo(w.p2ax, w.p2ay);
208        path.close();
209    }
210
211    private final WorkingSet mWorkingSet = new WorkingSet();
212    private final Path mPath = new Path();
213
214    /**
215     * Draw gesture preview trail
216     * @param canvas The canvas to draw the gesture preview trail
217     * @param paint The paint object to be used to draw the gesture preview trail
218     * @param outBoundsRect the bounding box of this gesture trail drawing
219     * @param params The drawing parameters of gesture preview trail
220     * @return true if some gesture preview trails remain to be drawn
221     */
222    public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
223            final Rect outBoundsRect, final Params params) {
224        final int trailSize = mEventTimes.getLength();
225        if (trailSize == 0) {
226            return false;
227        }
228
229        final int[] eventTimes = mEventTimes.getPrimitiveArray();
230        final int[] xCoords = mXCoordinates.getPrimitiveArray();
231        final int[] yCoords = mYCoordinates.getPrimitiveArray();
232        final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentTimeBase);
233        int startIndex;
234        for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) {
235            final int elapsedTime = sinceDown - eventTimes[startIndex];
236            // Skip too old trail points.
237            if (elapsedTime < params.mTrailLingerDuration) {
238                break;
239            }
240        }
241        mTrailStartIndex = startIndex;
242
243        if (startIndex < trailSize) {
244            paint.setColor(params.mTrailColor);
245            paint.setStyle(Paint.Style.FILL);
246            final Path path = mPath;
247            final WorkingSet w = mWorkingSet;
248            w.p1x = getXCoordValue(xCoords[startIndex]);
249            w.p1y = yCoords[startIndex];
250            int lastTime = sinceDown - eventTimes[startIndex];
251            float maxWidth = getWidth(lastTime, params);
252            w.r1 = maxWidth / 2.0f;
253            // Initialize bounds rectangle.
254            outBoundsRect.set((int)w.p1x, (int)w.p1y, (int)w.p1x, (int)w.p1y);
255            for (int i = startIndex + 1; i < trailSize - 1; i++) {
256                final int elapsedTime = sinceDown - eventTimes[i];
257                w.p2x = getXCoordValue(xCoords[i]);
258                w.p2y = yCoords[i];
259                // Draw trail line only when the current point isn't a down point.
260                if (!isDownEventXCoord(xCoords[i])) {
261                    final int alpha = getAlpha(elapsedTime, params);
262                    paint.setAlpha(alpha);
263                    final float width = getWidth(elapsedTime, params);
264                    w.r2 = width / 2.0f;
265                    if (calculatePathPoints(w)) {
266                        createPath(path, w);
267                        canvas.drawPath(path, paint);
268                        outBoundsRect.union((int)w.p2x, (int)w.p2y);
269                    }
270                    // Take union for the bounds.
271                    maxWidth = Math.max(maxWidth, width);
272                }
273                w.p1x = w.p2x;
274                w.p1y = w.p2y;
275                w.r1 = w.r2;
276                lastTime = elapsedTime;
277            }
278            // Take care of trail line width.
279            final int inset = -((int)maxWidth + 1);
280            outBoundsRect.inset(inset, inset);
281        }
282
283        final int newSize = trailSize - startIndex;
284        if (newSize < startIndex) {
285            mTrailStartIndex = 0;
286            if (newSize > 0) {
287                System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize);
288                System.arraycopy(xCoords, startIndex, xCoords, 0, newSize);
289                System.arraycopy(yCoords, startIndex, yCoords, 0, newSize);
290            }
291            mEventTimes.setLength(newSize);
292            mXCoordinates.setLength(newSize);
293            mYCoordinates.setLength(newSize);
294        }
295        return newSize > 0;
296    }
297}
298