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