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.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  */
     38 final 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