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