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"); you may not use this file except
      5  * in compliance with the License. You may obtain a copy of the License at
      6  *
      7  * http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the License
     10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
     11  * or implied. See the License for the specific language governing permissions and limitations under
     12  * the License.
     13  */
     14 
     15 package com.android.inputmethod.keyboard.internal;
     16 
     17 import android.content.res.TypedArray;
     18 import android.util.Log;
     19 
     20 import com.android.inputmethod.latin.InputPointers;
     21 import com.android.inputmethod.latin.R;
     22 import com.android.inputmethod.latin.ResizableIntArray;
     23 import com.android.inputmethod.latin.ResourceUtils;
     24 
     25 public class GestureStroke {
     26     private static final String TAG = GestureStroke.class.getSimpleName();
     27     private static final boolean DEBUG = false;
     28     private static final boolean DEBUG_SPEED = false;
     29 
     30     public static final int DEFAULT_CAPACITY = 128;
     31 
     32     private final int mPointerId;
     33     private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
     34     private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     35     private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     36 
     37     private final GestureStrokeParams mParams;
     38 
     39     private int mKeyWidth; // pixel
     40     // Static threshold for starting gesture detection
     41     private int mDetectFastMoveSpeedThreshold; // pixel /sec
     42     private int mDetectFastMoveTime;
     43     private int mDetectFastMoveX;
     44     private int mDetectFastMoveY;
     45     // Dynamic threshold for gesture after fast typing
     46     private boolean mAfterFastTyping;
     47     private int mGestureDynamicDistanceThresholdFrom; // pixel
     48     private int mGestureDynamicDistanceThresholdTo; // pixel
     49     // Variables for gesture sampling
     50     private int mGestureSamplingMinimumDistance; // pixel
     51     private long mLastMajorEventTime;
     52     private int mLastMajorEventX;
     53     private int mLastMajorEventY;
     54     // Variables for gesture recognition
     55     private int mGestureRecognitionSpeedThreshold; // pixel / sec
     56     private int mIncrementalRecognitionSize;
     57     private int mLastIncrementalBatchSize;
     58 
     59     public static final class GestureStrokeParams {
     60         // Static threshold for gesture after fast typing
     61         public final int mStaticTimeThresholdAfterFastTyping; // msec
     62         // Static threshold for starting gesture detection
     63         public final float mDetectFastMoveSpeedThreshold; // keyWidth/sec
     64         // Dynamic threshold for gesture after fast typing
     65         public final int mDynamicThresholdDecayDuration; // msec
     66         // Time based threshold values
     67         public final int mDynamicTimeThresholdFrom; // msec
     68         public final int mDynamicTimeThresholdTo; // msec
     69         // Distance based threshold values
     70         public final float mDynamicDistanceThresholdFrom; // keyWidth
     71         public final float mDynamicDistanceThresholdTo; // keyWidth
     72         // Parameters for gesture sampling
     73         public final float mSamplingMinimumDistance; // keyWidth
     74         // Parameters for gesture recognition
     75         public final int mRecognitionMinimumTime; // msec
     76         public final float mRecognitionSpeedThreshold; // keyWidth/sec
     77 
     78         // Default GestureStroke parameters for test.
     79         public static final GestureStrokeParams FOR_TEST = new GestureStrokeParams();
     80         public static final GestureStrokeParams DEFAULT = FOR_TEST;
     81 
     82         private GestureStrokeParams() {
     83             // These parameter values are default and intended for testing.
     84             mStaticTimeThresholdAfterFastTyping = 350; // msec
     85             mDetectFastMoveSpeedThreshold = 1.5f; // keyWidth / sec
     86             mDynamicThresholdDecayDuration = 450; // msec
     87             mDynamicTimeThresholdFrom = 300; // msec
     88             mDynamicTimeThresholdTo = 20; // msec
     89             mDynamicDistanceThresholdFrom = 6.0f; // keyWidth
     90             mDynamicDistanceThresholdTo = 0.35f; // keyWidth
     91             // The following parameters' change will affect the result of regression test.
     92             mSamplingMinimumDistance = 1.0f / 6.0f; // keyWidth
     93             mRecognitionMinimumTime = 100; // msec
     94             mRecognitionSpeedThreshold = 5.5f; // keyWidth / sec
     95         }
     96 
     97         public GestureStrokeParams(final TypedArray mainKeyboardViewAttr) {
     98             mStaticTimeThresholdAfterFastTyping = mainKeyboardViewAttr.getInt(
     99                     R.styleable.MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping,
    100                     DEFAULT.mStaticTimeThresholdAfterFastTyping);
    101             mDetectFastMoveSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr,
    102                     R.styleable.MainKeyboardView_gestureDetectFastMoveSpeedThreshold,
    103                     DEFAULT.mDetectFastMoveSpeedThreshold);
    104             mDynamicThresholdDecayDuration = mainKeyboardViewAttr.getInt(
    105                     R.styleable.MainKeyboardView_gestureDynamicThresholdDecayDuration,
    106                     DEFAULT.mDynamicThresholdDecayDuration);
    107             mDynamicTimeThresholdFrom = mainKeyboardViewAttr.getInt(
    108                     R.styleable.MainKeyboardView_gestureDynamicTimeThresholdFrom,
    109                     DEFAULT.mDynamicTimeThresholdFrom);
    110             mDynamicTimeThresholdTo = mainKeyboardViewAttr.getInt(
    111                     R.styleable.MainKeyboardView_gestureDynamicTimeThresholdTo,
    112                     DEFAULT.mDynamicTimeThresholdTo);
    113             mDynamicDistanceThresholdFrom = ResourceUtils.getFraction(mainKeyboardViewAttr,
    114                     R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdFrom,
    115                     DEFAULT.mDynamicDistanceThresholdFrom);
    116             mDynamicDistanceThresholdTo = ResourceUtils.getFraction(mainKeyboardViewAttr,
    117                     R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdTo,
    118                     DEFAULT.mDynamicDistanceThresholdTo);
    119             mSamplingMinimumDistance = ResourceUtils.getFraction(mainKeyboardViewAttr,
    120                     R.styleable.MainKeyboardView_gestureSamplingMinimumDistance,
    121                     DEFAULT.mSamplingMinimumDistance);
    122             mRecognitionMinimumTime = mainKeyboardViewAttr.getInt(
    123                     R.styleable.MainKeyboardView_gestureRecognitionMinimumTime,
    124                     DEFAULT.mRecognitionMinimumTime);
    125             mRecognitionSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr,
    126                     R.styleable.MainKeyboardView_gestureRecognitionSpeedThreshold,
    127                     DEFAULT.mRecognitionSpeedThreshold);
    128         }
    129     }
    130 
    131     private static final int MSEC_PER_SEC = 1000;
    132 
    133     public GestureStroke(final int pointerId, final GestureStrokeParams params) {
    134         mPointerId = pointerId;
    135         mParams = params;
    136     }
    137 
    138     public void setKeyboardGeometry(final int keyWidth) {
    139         mKeyWidth = keyWidth;
    140         // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
    141         mDetectFastMoveSpeedThreshold = (int)(keyWidth * mParams.mDetectFastMoveSpeedThreshold);
    142         mGestureDynamicDistanceThresholdFrom =
    143                 (int)(keyWidth * mParams.mDynamicDistanceThresholdFrom);
    144         mGestureDynamicDistanceThresholdTo = (int)(keyWidth * mParams.mDynamicDistanceThresholdTo);
    145         mGestureSamplingMinimumDistance = (int)(keyWidth * mParams.mSamplingMinimumDistance);
    146         mGestureRecognitionSpeedThreshold = (int)(keyWidth * mParams.mRecognitionSpeedThreshold);
    147         if (DEBUG) {
    148             Log.d(TAG, String.format(
    149                     "[%d] setKeyboardGeometry: keyWidth=%3d tT=%3d >> %3d tD=%3d >> %3d",
    150                     mPointerId, keyWidth,
    151                     mParams.mDynamicTimeThresholdFrom,
    152                     mParams.mDynamicTimeThresholdTo,
    153                     mGestureDynamicDistanceThresholdFrom,
    154                     mGestureDynamicDistanceThresholdTo));
    155         }
    156     }
    157 
    158     public void onDownEvent(final int x, final int y, final long downTime,
    159             final long gestureFirstDownTime, final long lastTypingTime) {
    160         reset();
    161         final long elapsedTimeAfterTyping = downTime - lastTypingTime;
    162         if (elapsedTimeAfterTyping < mParams.mStaticTimeThresholdAfterFastTyping) {
    163             mAfterFastTyping = true;
    164         }
    165         if (DEBUG) {
    166             Log.d(TAG, String.format("[%d] onDownEvent: dT=%3d%s", mPointerId,
    167                     elapsedTimeAfterTyping, mAfterFastTyping ? " afterFastTyping" : ""));
    168         }
    169         final int elapsedTimeFromFirstDown = (int)(downTime - gestureFirstDownTime);
    170         addPoint(x, y, elapsedTimeFromFirstDown, true /* isMajorEvent */);
    171     }
    172 
    173     private int getGestureDynamicDistanceThreshold(final int deltaTime) {
    174         if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) {
    175             return mGestureDynamicDistanceThresholdTo;
    176         }
    177         final int decayedThreshold =
    178                 (mGestureDynamicDistanceThresholdFrom - mGestureDynamicDistanceThresholdTo)
    179                 * deltaTime / mParams.mDynamicThresholdDecayDuration;
    180         return mGestureDynamicDistanceThresholdFrom - decayedThreshold;
    181     }
    182 
    183     private int getGestureDynamicTimeThreshold(final int deltaTime) {
    184         if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) {
    185             return mParams.mDynamicTimeThresholdTo;
    186         }
    187         final int decayedThreshold =
    188                 (mParams.mDynamicTimeThresholdFrom - mParams.mDynamicTimeThresholdTo)
    189                 * deltaTime / mParams.mDynamicThresholdDecayDuration;
    190         return mParams.mDynamicTimeThresholdFrom - decayedThreshold;
    191     }
    192 
    193     public final boolean isStartOfAGesture() {
    194         if (!hasDetectedFastMove()) {
    195             return false;
    196         }
    197         final int size = mEventTimes.getLength();
    198         if (size <= 0) {
    199             return false;
    200         }
    201         final int lastIndex = size - 1;
    202         final int deltaTime = mEventTimes.get(lastIndex) - mDetectFastMoveTime;
    203         if (deltaTime < 0) {
    204             return false;
    205         }
    206         final int deltaDistance = getDistance(
    207                 mXCoordinates.get(lastIndex), mYCoordinates.get(lastIndex),
    208                 mDetectFastMoveX, mDetectFastMoveY);
    209         final int distanceThreshold = getGestureDynamicDistanceThreshold(deltaTime);
    210         final int timeThreshold = getGestureDynamicTimeThreshold(deltaTime);
    211         final boolean isStartOfAGesture = deltaTime >= timeThreshold
    212                 && deltaDistance >= distanceThreshold;
    213         if (DEBUG) {
    214             Log.d(TAG, String.format("[%d] isStartOfAGesture: dT=%3d tT=%3d dD=%3d tD=%3d%s%s",
    215                     mPointerId, deltaTime, timeThreshold,
    216                     deltaDistance, distanceThreshold,
    217                     mAfterFastTyping ? " afterFastTyping" : "",
    218                     isStartOfAGesture ? " startOfAGesture" : ""));
    219         }
    220         return isStartOfAGesture;
    221     }
    222 
    223     protected void reset() {
    224         mIncrementalRecognitionSize = 0;
    225         mLastIncrementalBatchSize = 0;
    226         mEventTimes.setLength(0);
    227         mXCoordinates.setLength(0);
    228         mYCoordinates.setLength(0);
    229         mLastMajorEventTime = 0;
    230         mDetectFastMoveTime = 0;
    231         mAfterFastTyping = false;
    232     }
    233 
    234     private void appendPoint(final int x, final int y, final int time) {
    235         mEventTimes.add(time);
    236         mXCoordinates.add(x);
    237         mYCoordinates.add(y);
    238     }
    239 
    240     private void updateMajorEvent(final int x, final int y, final int time) {
    241         mLastMajorEventTime = time;
    242         mLastMajorEventX = x;
    243         mLastMajorEventY = y;
    244     }
    245 
    246     private final boolean hasDetectedFastMove() {
    247         return mDetectFastMoveTime > 0;
    248     }
    249 
    250     private int detectFastMove(final int x, final int y, final int time) {
    251         final int size = mEventTimes.getLength();
    252         final int lastIndex = size - 1;
    253         final int lastX = mXCoordinates.get(lastIndex);
    254         final int lastY = mYCoordinates.get(lastIndex);
    255         final int dist = getDistance(lastX, lastY, x, y);
    256         final int msecs = time - mEventTimes.get(lastIndex);
    257         if (msecs > 0) {
    258             final int pixels = getDistance(lastX, lastY, x, y);
    259             final int pixelsPerSec = pixels * MSEC_PER_SEC;
    260             if (DEBUG_SPEED) {
    261                 final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
    262                 Log.d(TAG, String.format("[%d] detectFastMove: speed=%5.2f", mPointerId, speed));
    263             }
    264             // Equivalent to (pixels / msecs < mStartSpeedThreshold / MSEC_PER_SEC)
    265             if (!hasDetectedFastMove() && pixelsPerSec > mDetectFastMoveSpeedThreshold * msecs) {
    266                 if (DEBUG) {
    267                     final float speed = (float)pixelsPerSec / msecs / mKeyWidth;
    268                     Log.d(TAG, String.format(
    269                             "[%d] detectFastMove: speed=%5.2f T=%3d points=%3d fastMove",
    270                             mPointerId, speed, time, size));
    271                 }
    272                 mDetectFastMoveTime = time;
    273                 mDetectFastMoveX = x;
    274                 mDetectFastMoveY = y;
    275             }
    276         }
    277         return dist;
    278     }
    279 
    280     public void addPoint(final int x, final int y, final int time, final boolean isMajorEvent) {
    281         final int size = mEventTimes.getLength();
    282         if (size <= 0) {
    283             // Down event
    284             appendPoint(x, y, time);
    285             updateMajorEvent(x, y, time);
    286         } else {
    287             final int distance = detectFastMove(x, y, time);
    288             if (distance > mGestureSamplingMinimumDistance) {
    289                 appendPoint(x, y, time);
    290             }
    291         }
    292         if (isMajorEvent) {
    293             updateIncrementalRecognitionSize(x, y, time);
    294             updateMajorEvent(x, y, time);
    295         }
    296     }
    297 
    298     private void updateIncrementalRecognitionSize(final int x, final int y, final int time) {
    299         final int msecs = (int)(time - mLastMajorEventTime);
    300         if (msecs <= 0) {
    301             return;
    302         }
    303         final int pixels = getDistance(mLastMajorEventX, mLastMajorEventY, x, y);
    304         final int pixelsPerSec = pixels * MSEC_PER_SEC;
    305         // Equivalent to (pixels / msecs < mGestureRecognitionThreshold / MSEC_PER_SEC)
    306         if (pixelsPerSec < mGestureRecognitionSpeedThreshold * msecs) {
    307             mIncrementalRecognitionSize = mEventTimes.getLength();
    308         }
    309     }
    310 
    311     public final boolean hasRecognitionTimePast(
    312             final long currentTime, final long lastRecognitionTime) {
    313         return currentTime > lastRecognitionTime + mParams.mRecognitionMinimumTime;
    314     }
    315 
    316     public final void appendAllBatchPoints(final InputPointers out) {
    317         appendBatchPoints(out, mEventTimes.getLength());
    318     }
    319 
    320     public final void appendIncrementalBatchPoints(final InputPointers out) {
    321         appendBatchPoints(out, mIncrementalRecognitionSize);
    322     }
    323 
    324     private void appendBatchPoints(final InputPointers out, final int size) {
    325         final int length = size - mLastIncrementalBatchSize;
    326         if (length <= 0) {
    327             return;
    328         }
    329         out.append(mPointerId, mEventTimes, mXCoordinates, mYCoordinates,
    330                 mLastIncrementalBatchSize, length);
    331         mLastIncrementalBatchSize = size;
    332     }
    333 
    334     private static int getDistance(final int x1, final int y1, final int x2, final int y2) {
    335         final int dx = x1 - x2;
    336         final int dy = y1 - y2;
    337         // Note that, in recent versions of Android, FloatMath is actually slower than
    338         // java.lang.Math due to the way the JIT optimizes java.lang.Math.
    339         return (int)Math.sqrt(dx * dx + dy * dy);
    340     }
    341 }
    342