| /* |
| * 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.util.Log; |
| |
| import com.android.inputmethod.latin.InputPointers; |
| import com.android.inputmethod.latin.R; |
| import com.android.inputmethod.latin.ResizableIntArray; |
| import com.android.inputmethod.latin.ResourceUtils; |
| |
| public class GestureStroke { |
| private static final String TAG = GestureStroke.class.getSimpleName(); |
| private static final boolean DEBUG = false; |
| private static final boolean DEBUG_SPEED = false; |
| |
| // The height of extra area above the keyboard to draw gesture trails. |
| // Proportional to the keyboard height. |
| public static final float EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO = 0.25f; |
| |
| public static final int DEFAULT_CAPACITY = 128; |
| |
| private final int mPointerId; |
| private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY); |
| private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); |
| private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY); |
| |
| private final GestureStrokeParams mParams; |
| |
| private int mKeyWidth; // pixel |
| private int mMinYCoordinate; // pixel |
| private int mMaxYCoordinate; // pixel |
| // Static threshold for starting gesture detection |
| private int mDetectFastMoveSpeedThreshold; // pixel /sec |
| private int mDetectFastMoveTime; |
| private int mDetectFastMoveX; |
| private int mDetectFastMoveY; |
| // Dynamic threshold for gesture after fast typing |
| private boolean mAfterFastTyping; |
| private int mGestureDynamicDistanceThresholdFrom; // pixel |
| private int mGestureDynamicDistanceThresholdTo; // pixel |
| // Variables for gesture sampling |
| private int mGestureSamplingMinimumDistance; // pixel |
| private long mLastMajorEventTime; |
| private int mLastMajorEventX; |
| private int mLastMajorEventY; |
| // Variables for gesture recognition |
| private int mGestureRecognitionSpeedThreshold; // pixel / sec |
| private int mIncrementalRecognitionSize; |
| private int mLastIncrementalBatchSize; |
| |
| public static final class GestureStrokeParams { |
| // Static threshold for gesture after fast typing |
| public final int mStaticTimeThresholdAfterFastTyping; // msec |
| // Static threshold for starting gesture detection |
| public final float mDetectFastMoveSpeedThreshold; // keyWidth/sec |
| // Dynamic threshold for gesture after fast typing |
| public final int mDynamicThresholdDecayDuration; // msec |
| // Time based threshold values |
| public final int mDynamicTimeThresholdFrom; // msec |
| public final int mDynamicTimeThresholdTo; // msec |
| // Distance based threshold values |
| public final float mDynamicDistanceThresholdFrom; // keyWidth |
| public final float mDynamicDistanceThresholdTo; // keyWidth |
| // Parameters for gesture sampling |
| public final float mSamplingMinimumDistance; // keyWidth |
| // Parameters for gesture recognition |
| public final int mRecognitionMinimumTime; // msec |
| public final float mRecognitionSpeedThreshold; // keyWidth/sec |
| |
| // Default GestureStroke parameters. |
| public static final GestureStrokeParams DEFAULT = new GestureStrokeParams(); |
| |
| private GestureStrokeParams() { |
| // These parameter values are default and intended for testing. |
| mStaticTimeThresholdAfterFastTyping = 350; // msec |
| mDetectFastMoveSpeedThreshold = 1.5f; // keyWidth / sec |
| mDynamicThresholdDecayDuration = 450; // msec |
| mDynamicTimeThresholdFrom = 300; // msec |
| mDynamicTimeThresholdTo = 20; // msec |
| mDynamicDistanceThresholdFrom = 6.0f; // keyWidth |
| mDynamicDistanceThresholdTo = 0.35f; // keyWidth |
| // The following parameters' change will affect the result of regression test. |
| mSamplingMinimumDistance = 1.0f / 6.0f; // keyWidth |
| mRecognitionMinimumTime = 100; // msec |
| mRecognitionSpeedThreshold = 5.5f; // keyWidth / sec |
| } |
| |
| public GestureStrokeParams(final TypedArray mainKeyboardViewAttr) { |
| mStaticTimeThresholdAfterFastTyping = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping, |
| DEFAULT.mStaticTimeThresholdAfterFastTyping); |
| mDetectFastMoveSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, |
| R.styleable.MainKeyboardView_gestureDetectFastMoveSpeedThreshold, |
| DEFAULT.mDetectFastMoveSpeedThreshold); |
| mDynamicThresholdDecayDuration = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_gestureDynamicThresholdDecayDuration, |
| DEFAULT.mDynamicThresholdDecayDuration); |
| mDynamicTimeThresholdFrom = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_gestureDynamicTimeThresholdFrom, |
| DEFAULT.mDynamicTimeThresholdFrom); |
| mDynamicTimeThresholdTo = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_gestureDynamicTimeThresholdTo, |
| DEFAULT.mDynamicTimeThresholdTo); |
| mDynamicDistanceThresholdFrom = ResourceUtils.getFraction(mainKeyboardViewAttr, |
| R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdFrom, |
| DEFAULT.mDynamicDistanceThresholdFrom); |
| mDynamicDistanceThresholdTo = ResourceUtils.getFraction(mainKeyboardViewAttr, |
| R.styleable.MainKeyboardView_gestureDynamicDistanceThresholdTo, |
| DEFAULT.mDynamicDistanceThresholdTo); |
| mSamplingMinimumDistance = ResourceUtils.getFraction(mainKeyboardViewAttr, |
| R.styleable.MainKeyboardView_gestureSamplingMinimumDistance, |
| DEFAULT.mSamplingMinimumDistance); |
| mRecognitionMinimumTime = mainKeyboardViewAttr.getInt( |
| R.styleable.MainKeyboardView_gestureRecognitionMinimumTime, |
| DEFAULT.mRecognitionMinimumTime); |
| mRecognitionSpeedThreshold = ResourceUtils.getFraction(mainKeyboardViewAttr, |
| R.styleable.MainKeyboardView_gestureRecognitionSpeedThreshold, |
| DEFAULT.mRecognitionSpeedThreshold); |
| } |
| } |
| |
| private static final int MSEC_PER_SEC = 1000; |
| |
| public GestureStroke(final int pointerId, final GestureStrokeParams params) { |
| mPointerId = pointerId; |
| mParams = params; |
| } |
| |
| public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) { |
| mKeyWidth = keyWidth; |
| mMinYCoordinate = -(int)(keyboardHeight * EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO); |
| mMaxYCoordinate = keyboardHeight - 1; |
| // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key? |
| mDetectFastMoveSpeedThreshold = (int)(keyWidth * mParams.mDetectFastMoveSpeedThreshold); |
| mGestureDynamicDistanceThresholdFrom = |
| (int)(keyWidth * mParams.mDynamicDistanceThresholdFrom); |
| mGestureDynamicDistanceThresholdTo = (int)(keyWidth * mParams.mDynamicDistanceThresholdTo); |
| mGestureSamplingMinimumDistance = (int)(keyWidth * mParams.mSamplingMinimumDistance); |
| mGestureRecognitionSpeedThreshold = (int)(keyWidth * mParams.mRecognitionSpeedThreshold); |
| if (DEBUG) { |
| Log.d(TAG, String.format( |
| "[%d] setKeyboardGeometry: keyWidth=%3d tT=%3d >> %3d tD=%3d >> %3d", |
| mPointerId, keyWidth, |
| mParams.mDynamicTimeThresholdFrom, |
| mParams.mDynamicTimeThresholdTo, |
| mGestureDynamicDistanceThresholdFrom, |
| mGestureDynamicDistanceThresholdTo)); |
| } |
| } |
| |
| public int getLength() { |
| return mEventTimes.getLength(); |
| } |
| |
| public void onDownEvent(final int x, final int y, final long downTime, |
| final long gestureFirstDownTime, final long lastTypingTime) { |
| reset(); |
| final long elapsedTimeAfterTyping = downTime - lastTypingTime; |
| if (elapsedTimeAfterTyping < mParams.mStaticTimeThresholdAfterFastTyping) { |
| mAfterFastTyping = true; |
| } |
| if (DEBUG) { |
| Log.d(TAG, String.format("[%d] onDownEvent: dT=%3d%s", mPointerId, |
| elapsedTimeAfterTyping, mAfterFastTyping ? " afterFastTyping" : "")); |
| } |
| final int elapsedTimeFromFirstDown = (int)(downTime - gestureFirstDownTime); |
| addPointOnKeyboard(x, y, elapsedTimeFromFirstDown, true /* isMajorEvent */); |
| } |
| |
| private int getGestureDynamicDistanceThreshold(final int deltaTime) { |
| if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) { |
| return mGestureDynamicDistanceThresholdTo; |
| } |
| final int decayedThreshold = |
| (mGestureDynamicDistanceThresholdFrom - mGestureDynamicDistanceThresholdTo) |
| * deltaTime / mParams.mDynamicThresholdDecayDuration; |
| return mGestureDynamicDistanceThresholdFrom - decayedThreshold; |
| } |
| |
| private int getGestureDynamicTimeThreshold(final int deltaTime) { |
| if (!mAfterFastTyping || deltaTime >= mParams.mDynamicThresholdDecayDuration) { |
| return mParams.mDynamicTimeThresholdTo; |
| } |
| final int decayedThreshold = |
| (mParams.mDynamicTimeThresholdFrom - mParams.mDynamicTimeThresholdTo) |
| * deltaTime / mParams.mDynamicThresholdDecayDuration; |
| return mParams.mDynamicTimeThresholdFrom - decayedThreshold; |
| } |
| |
| public final boolean isStartOfAGesture() { |
| if (!hasDetectedFastMove()) { |
| return false; |
| } |
| final int size = getLength(); |
| if (size <= 0) { |
| return false; |
| } |
| final int lastIndex = size - 1; |
| final int deltaTime = mEventTimes.get(lastIndex) - mDetectFastMoveTime; |
| if (deltaTime < 0) { |
| return false; |
| } |
| final int deltaDistance = getDistance( |
| mXCoordinates.get(lastIndex), mYCoordinates.get(lastIndex), |
| mDetectFastMoveX, mDetectFastMoveY); |
| final int distanceThreshold = getGestureDynamicDistanceThreshold(deltaTime); |
| final int timeThreshold = getGestureDynamicTimeThreshold(deltaTime); |
| final boolean isStartOfAGesture = deltaTime >= timeThreshold |
| && deltaDistance >= distanceThreshold; |
| if (DEBUG) { |
| Log.d(TAG, String.format("[%d] isStartOfAGesture: dT=%3d tT=%3d dD=%3d tD=%3d%s%s", |
| mPointerId, deltaTime, timeThreshold, |
| deltaDistance, distanceThreshold, |
| mAfterFastTyping ? " afterFastTyping" : "", |
| isStartOfAGesture ? " startOfAGesture" : "")); |
| } |
| return isStartOfAGesture; |
| } |
| |
| public void duplicateLastPointWith(final int time) { |
| final int lastIndex = getLength() - 1; |
| if (lastIndex >= 0) { |
| final int x = mXCoordinates.get(lastIndex); |
| final int y = mYCoordinates.get(lastIndex); |
| if (DEBUG) { |
| Log.d(TAG, String.format("[%d] duplicateLastPointWith: %d,%d|%d", mPointerId, |
| x, y, time)); |
| } |
| // TODO: Have appendMajorPoint() |
| appendPoint(x, y, time); |
| updateIncrementalRecognitionSize(x, y, time); |
| } |
| } |
| |
| protected void reset() { |
| mIncrementalRecognitionSize = 0; |
| mLastIncrementalBatchSize = 0; |
| mEventTimes.setLength(0); |
| mXCoordinates.setLength(0); |
| mYCoordinates.setLength(0); |
| mLastMajorEventTime = 0; |
| mDetectFastMoveTime = 0; |
| mAfterFastTyping = false; |
| } |
| |
| private void appendPoint(final int x, final int y, final int time) { |
| final int lastIndex = getLength() - 1; |
| // The point that is created by {@link duplicateLastPointWith(int)} may have later event |
| // time than the next {@link MotionEvent}. To maintain the monotonicity of the event time, |
| // drop the successive point here. |
| if (lastIndex >= 0 && mEventTimes.get(lastIndex) > time) { |
| Log.w(TAG, String.format("[%d] drop stale event: %d,%d|%d last: %d,%d|%d", mPointerId, |
| x, y, time, mXCoordinates.get(lastIndex), mYCoordinates.get(lastIndex), |
| mEventTimes.get(lastIndex))); |
| return; |
| } |
| mEventTimes.add(time); |
| mXCoordinates.add(x); |
| mYCoordinates.add(y); |
| } |
| |
| private void updateMajorEvent(final int x, final int y, final int time) { |
| mLastMajorEventTime = time; |
| mLastMajorEventX = x; |
| mLastMajorEventY = y; |
| } |
| |
| private final boolean hasDetectedFastMove() { |
| return mDetectFastMoveTime > 0; |
| } |
| |
| private int detectFastMove(final int x, final int y, final int time) { |
| final int size = getLength(); |
| final int lastIndex = size - 1; |
| final int lastX = mXCoordinates.get(lastIndex); |
| final int lastY = mYCoordinates.get(lastIndex); |
| final int dist = getDistance(lastX, lastY, x, y); |
| final int msecs = time - mEventTimes.get(lastIndex); |
| if (msecs > 0) { |
| final int pixels = getDistance(lastX, lastY, x, y); |
| final int pixelsPerSec = pixels * MSEC_PER_SEC; |
| if (DEBUG_SPEED) { |
| final float speed = (float)pixelsPerSec / msecs / mKeyWidth; |
| Log.d(TAG, String.format("[%d] detectFastMove: speed=%5.2f", mPointerId, speed)); |
| } |
| // Equivalent to (pixels / msecs < mStartSpeedThreshold / MSEC_PER_SEC) |
| if (!hasDetectedFastMove() && pixelsPerSec > mDetectFastMoveSpeedThreshold * msecs) { |
| if (DEBUG) { |
| final float speed = (float)pixelsPerSec / msecs / mKeyWidth; |
| Log.d(TAG, String.format( |
| "[%d] detectFastMove: speed=%5.2f T=%3d points=%3d fastMove", |
| mPointerId, speed, time, size)); |
| } |
| mDetectFastMoveTime = time; |
| mDetectFastMoveX = x; |
| mDetectFastMoveY = y; |
| } |
| } |
| return dist; |
| } |
| |
| /** |
| * Add a touch event as a gesture point. Returns true if the touch event is on the valid |
| * gesture area. |
| * @param x the x-coordinate of the touch event |
| * @param y the y-coordinate of the touch event |
| * @param time the elapsed time in millisecond from the first gesture down |
| * @param isMajorEvent false if this is a historical move event |
| * @return true if the touch event is on the valid gesture area |
| */ |
| public boolean addPointOnKeyboard(final int x, final int y, final int time, |
| final boolean isMajorEvent) { |
| final int size = getLength(); |
| if (size <= 0) { |
| // Down event |
| appendPoint(x, y, time); |
| updateMajorEvent(x, y, time); |
| } else { |
| final int distance = detectFastMove(x, y, time); |
| if (distance > mGestureSamplingMinimumDistance) { |
| appendPoint(x, y, time); |
| } |
| } |
| if (isMajorEvent) { |
| updateIncrementalRecognitionSize(x, y, time); |
| updateMajorEvent(x, y, time); |
| } |
| return y >= mMinYCoordinate && y < mMaxYCoordinate; |
| } |
| |
| private void updateIncrementalRecognitionSize(final int x, final int y, final int time) { |
| final int msecs = (int)(time - mLastMajorEventTime); |
| if (msecs <= 0) { |
| return; |
| } |
| final int pixels = getDistance(mLastMajorEventX, mLastMajorEventY, x, y); |
| final int pixelsPerSec = pixels * MSEC_PER_SEC; |
| // Equivalent to (pixels / msecs < mGestureRecognitionThreshold / MSEC_PER_SEC) |
| if (pixelsPerSec < mGestureRecognitionSpeedThreshold * msecs) { |
| mIncrementalRecognitionSize = getLength(); |
| } |
| } |
| |
| public final boolean hasRecognitionTimePast( |
| final long currentTime, final long lastRecognitionTime) { |
| return currentTime > lastRecognitionTime + mParams.mRecognitionMinimumTime; |
| } |
| |
| public final void appendAllBatchPoints(final InputPointers out) { |
| appendBatchPoints(out, getLength()); |
| } |
| |
| public final void appendIncrementalBatchPoints(final InputPointers out) { |
| appendBatchPoints(out, mIncrementalRecognitionSize); |
| } |
| |
| private void appendBatchPoints(final InputPointers out, final int size) { |
| final int length = size - mLastIncrementalBatchSize; |
| if (length <= 0) { |
| return; |
| } |
| out.append(mPointerId, mEventTimes, mXCoordinates, mYCoordinates, |
| mLastIncrementalBatchSize, length); |
| mLastIncrementalBatchSize = size; |
| } |
| |
| private static int getDistance(final int x1, final int y1, final int x2, final int y2) { |
| final int dx = x1 - x2; |
| final int dy = y1 - y2; |
| // Note that, in recent versions of Android, FloatMath is actually slower than |
| // java.lang.Math due to the way the JIT optimizes java.lang.Math. |
| return (int)Math.sqrt(dx * dx + dy * dy); |
| } |
| } |