/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.android.inputmethod.keyboard.internal; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.os.SystemClock; import com.android.inputmethod.latin.Constants; import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.ResizableIntArray; final class GesturePreviewTrail { private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY; private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY); private int mCurrentStrokeId = -1; // The wall time of the zero value in {@link #mEventTimes} private long mCurrentTimeBase; private int mTrailStartIndex; static final class Params { public final int mTrailColor; public final float mTrailStartWidth; public final float mTrailEndWidth; public final int mFadeoutStartDelay; public final int mFadeoutDuration; public final int mUpdateInterval; public final int mTrailLingerDuration; public Params(final TypedArray keyboardViewAttr) { mTrailColor = keyboardViewAttr.getColor( R.styleable.KeyboardView_gesturePreviewTrailColor, 0); mTrailStartWidth = keyboardViewAttr.getDimension( R.styleable.KeyboardView_gesturePreviewTrailStartWidth, 0.0f); mTrailEndWidth = keyboardViewAttr.getDimension( R.styleable.KeyboardView_gesturePreviewTrailEndWidth, 0.0f); mFadeoutStartDelay = keyboardViewAttr.getInt( R.styleable.KeyboardView_gesturePreviewTrailFadeoutStartDelay, 0); mFadeoutDuration = keyboardViewAttr.getInt( R.styleable.KeyboardView_gesturePreviewTrailFadeoutDuration, 0); mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration; mUpdateInterval = keyboardViewAttr.getInt( R.styleable.KeyboardView_gesturePreviewTrailUpdateInterval, 0); } } // Use this value as imaginary zero because x-coordinates may be zero. private static final int DOWN_EVENT_MARKER = -128; private static int markAsDownEvent(final int xCoord) { return DOWN_EVENT_MARKER - xCoord; } private static boolean isDownEventXCoord(final int xCoordOrMark) { return xCoordOrMark <= DOWN_EVENT_MARKER; } private static int getXCoordValue(final int xCoordOrMark) { return isDownEventXCoord(xCoordOrMark) ? DOWN_EVENT_MARKER - xCoordOrMark : xCoordOrMark; } public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) { final int trailSize = mEventTimes.getLength(); stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates); if (mEventTimes.getLength() == trailSize) { return; } final int[] eventTimes = mEventTimes.getPrimitiveArray(); final int strokeId = stroke.getGestureStrokeId(); if (strokeId != mCurrentStrokeId) { final int elapsedTime = (int)(downTime - mCurrentTimeBase); for (int i = mTrailStartIndex; i < trailSize; i++) { // Decay the previous strokes' event times. eventTimes[i] -= elapsedTime; } final int[] xCoords = mXCoordinates.getPrimitiveArray(); final int downIndex = trailSize; xCoords[downIndex] = markAsDownEvent(xCoords[downIndex]); mCurrentTimeBase = downTime - eventTimes[downIndex]; mCurrentStrokeId = strokeId; } } private static int getAlpha(final int elapsedTime, final Params params) { if (elapsedTime < params.mFadeoutStartDelay) { return Constants.Color.ALPHA_OPAQUE; } final int decreasingAlpha = Constants.Color.ALPHA_OPAQUE * (elapsedTime - params.mFadeoutStartDelay) / params.mFadeoutDuration; return Constants.Color.ALPHA_OPAQUE - decreasingAlpha; } private static float getWidth(final int elapsedTime, final Params params) { return Math.max((params.mTrailLingerDuration - elapsedTime) * (params.mTrailStartWidth - params.mTrailEndWidth) / params.mTrailLingerDuration, 0.0f); } static final class WorkingSet { // Input // Previous point (P1) coordinates and trail radius. public float p1x, p1y; public float r1; // Current point (P2) coordinates and trail radius. public float p2x, p2y; public float r2; // Closing point of arc at P1. private float p1ax, p1ay; // Opening point of arc at P1. private float p1bx, p1by; // Opening point of arc at P2. private float p2ax, p2ay; // Closing point of arc at P2. private float p2bx, p2by; // Start angle of the trail arcs. private float angle; // Sweep angle of the trail arc at P1. private float a1; private final RectF arc1 = new RectF(); // Sweep angle of the trail arc at P2. private float a2; private final RectF arc2 = new RectF(); private static final float RADIAN_TO_DEGREE = (float)(180.0d / Math.PI); private static final float RIGHT_ANGLE = (float)(Math.PI / 2.0d); public boolean calculatePathPoints() { final float dx = p2x - p1x; final float dy = p2y - p1y; // Distance of the points. final double l = Math.hypot(dx, dy); if (Double.compare(0.0d, l) == 0) { return false; } // Angle of the line p1-p2 final float a = (float)Math.atan2(dy, dx); // Difference of trail cap radius. final float dr = r2 - r1; // Variation of angle at trail cap. final float ar = (float)Math.asin(dr / l); // The start angle of trail cap arc at P1. final float aa = a - (RIGHT_ANGLE + ar); // The end angle of trail cap arc at P2. final float ab = a + (RIGHT_ANGLE + ar); final float cosa = (float)Math.cos(aa); final float sina = (float)Math.sin(aa); final float cosb = (float)Math.cos(ab); final float sinb = (float)Math.sin(ab); p1ax = p1x + r1 * cosa; p1ay = p1y + r1 * sina; p1bx = p1x + r1 * cosb; p1by = p1y + r1 * sinb; p2ax = p2x + r2 * cosa; p2ay = p2y + r2 * sina; p2bx = p2x + r2 * cosb; p2by = p2y + r2 * sinb; angle = aa * RADIAN_TO_DEGREE; final float ar2degree = ar * 2.0f * RADIAN_TO_DEGREE; a1 = -180.0f + ar2degree; a2 = 180.0f + ar2degree; arc1.set(p1x, p1y, p1x, p1y); arc1.inset(-r1, -r1); arc2.set(p2x, p2y, p2x, p2y); arc2.inset(-r2, -r2); return true; } public void createPath(final Path path) { path.rewind(); // Trail cap at P1. path.moveTo(p1x, p1y); path.arcTo(arc1, angle, a1); // Trail cap at P2. path.moveTo(p2x, p2y); path.arcTo(arc2, angle, a2); // Two trapezoids connecting P1 and P2. path.moveTo(p1ax, p1ay); path.lineTo(p1x, p1y); path.lineTo(p1bx, p1by); path.lineTo(p2bx, p2by); path.lineTo(p2x, p2y); path.lineTo(p2ax, p2ay); path.close(); } } private final WorkingSet mWorkingSet = new WorkingSet(); private final Path mPath = new Path(); /** * Draw gesture preview trail * @param canvas The canvas to draw the gesture preview trail * @param paint The paint object to be used to draw the gesture preview trail * @param outBoundsRect the bounding box of this gesture trail drawing * @param params The drawing parameters of gesture preview trail * @return true if some gesture preview trails remain to be drawn */ public boolean drawGestureTrail(final Canvas canvas, final Paint paint, final Rect outBoundsRect, final Params params) { final int trailSize = mEventTimes.getLength(); if (trailSize == 0) { return false; } final int[] eventTimes = mEventTimes.getPrimitiveArray(); final int[] xCoords = mXCoordinates.getPrimitiveArray(); final int[] yCoords = mYCoordinates.getPrimitiveArray(); final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentTimeBase); int startIndex; for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) { final int elapsedTime = sinceDown - eventTimes[startIndex]; // Skip too old trail points. if (elapsedTime < params.mTrailLingerDuration) { break; } } mTrailStartIndex = startIndex; if (startIndex < trailSize) { paint.setColor(params.mTrailColor); paint.setStyle(Paint.Style.FILL); final Path path = mPath; final WorkingSet w = mWorkingSet; w.p1x = getXCoordValue(xCoords[startIndex]); w.p1y = yCoords[startIndex]; int lastTime = sinceDown - eventTimes[startIndex]; float maxWidth = getWidth(lastTime, params); w.r1 = maxWidth / 2.0f; // Initialize bounds rectangle. outBoundsRect.set((int)w.p1x, (int)w.p1y, (int)w.p1x, (int)w.p1y); for (int i = startIndex + 1; i < trailSize - 1; i++) { final int elapsedTime = sinceDown - eventTimes[i]; w.p2x = getXCoordValue(xCoords[i]); w.p2y = yCoords[i]; // Draw trail line only when the current point isn't a down point. if (!isDownEventXCoord(xCoords[i])) { final int alpha = getAlpha(elapsedTime, params); paint.setAlpha(alpha); final float width = getWidth(elapsedTime, params); w.r2 = width / 2.0f; if (w.calculatePathPoints()) { w.createPath(path); canvas.drawPath(path, paint); outBoundsRect.union((int)w.p2x, (int)w.p2y); } // Take union for the bounds. maxWidth = Math.max(maxWidth, width); } w.p1x = w.p2x; w.p1y = w.p2y; w.r1 = w.r2; lastTime = elapsedTime; } // Take care of trail line width. final int inset = -((int)maxWidth + 1); outBoundsRect.inset(inset, inset); } final int newSize = trailSize - startIndex; if (newSize < startIndex) { mTrailStartIndex = 0; if (newSize > 0) { System.arraycopy(eventTimes, startIndex, eventTimes, 0, newSize); System.arraycopy(xCoords, startIndex, xCoords, 0, newSize); System.arraycopy(yCoords, startIndex, yCoords, 0, newSize); } mEventTimes.setLength(newSize); mXCoordinates.setLength(newSize); mYCoordinates.setLength(newSize); } return newSize > 0; } }