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 15 package com.android.inputmethod.keyboard.internal; 16 17 import android.content.res.TypedArray; 18 import android.graphics.Canvas; 19 import android.graphics.Paint; 20 import android.graphics.Path; 21 import android.graphics.Rect; 22 import android.graphics.RectF; 23 import android.os.SystemClock; 24 25 import com.android.inputmethod.latin.Constants; 26 import com.android.inputmethod.latin.R; 27 import com.android.inputmethod.latin.ResizableIntArray; 28 29 final 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