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