Home | History | Annotate | Download | only in internal
      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 
     17 package com.android.inputmethod.keyboard.internal;
     18 
     19 import android.graphics.Canvas;
     20 import android.graphics.Color;
     21 import android.graphics.Paint;
     22 import android.graphics.Path;
     23 import android.graphics.Rect;
     24 import android.os.SystemClock;
     25 
     26 import com.android.inputmethod.latin.common.Constants;
     27 import 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  */
     37 final 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