Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eea34598 authored by Tom Ouyang's avatar Tom Ouyang Committed by Tadashi G. Takaoka
Browse files

Merging minimal gesture input

Change-Id: Iee6ae48bb6309c2867b5d2e344fe7d86dfabd654
parent e9808694
Loading
Loading
Loading
Loading
+12 −2
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package com.android.inputmethod.keyboard;

import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.SuggestedWords;

public interface KeyboardActionListener {

    /**
@@ -64,13 +67,18 @@ public interface KeyboardActionListener {
     */
    public void onTextInput(CharSequence text);

    // TODO: Should move this method to some more appropriate interface.
    /**
     * Called when user started batch input.
     */
    public void onStartBatchInput();

    // TODO: Should move this method to some more appropriate interface.
    /**
     * Sends the batch input points data to get updated suggestions
     * @param batchPointers the batch input points representing the user input
     * @return updated suggestions that reflects the user input
     */
    public SuggestedWords onUpdateBatchInput(InputPointers batchPointers);

    /**
     * Sends a sequence of characters to the listener as batch input.
     *
@@ -101,6 +109,8 @@ public interface KeyboardActionListener {
        @Override
        public void onStartBatchInput() {}
        @Override
        public SuggestedWords onUpdateBatchInput(InputPointers batchPointers) { return null; }
        @Override
        public void onEndBatchInput(CharSequence text) {}
        @Override
        public void onCancelInput() {}
+76 −15
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

import com.android.inputmethod.keyboard.internal.GestureTracker;
import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.ResearchLogger;
@@ -161,6 +162,9 @@ public class PointerTracker {
    private static final KeyboardActionListener EMPTY_LISTENER =
            new KeyboardActionListener.Adapter();

    // Gesture tracker singleton instance
    private static final GestureTracker sGestureTracker = GestureTracker.getInstance();

    public static void init(boolean hasDistinctMultitouch,
            boolean needsPhantomSuddenMoveEventHack) {
        if (hasDistinctMultitouch) {
@@ -199,6 +203,7 @@ public class PointerTracker {
        for (final PointerTracker tracker : sTrackers) {
            tracker.mListener = listener;
        }
        GestureTracker.init(listener);
    }

    public static void setKeyDetector(KeyDetector keyDetector) {
@@ -207,6 +212,7 @@ public class PointerTracker {
            // Mark that keyboard layout has been changed.
            tracker.mKeyboardLayoutHasBeenChanged = true;
        }
        sGestureTracker.setKeyboard(keyDetector.getKeyboard());
    }

    public static void dismissAllKeyPreviews() {
@@ -233,6 +239,9 @@ public class PointerTracker {

    // Returns true if keyboard has been changed by this callback.
    private boolean callListenerOnPressAndCheckKeyboardLayoutChange(Key key) {
        if (sGestureTracker.isInGesture()) {
            return false;
        }
        final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
        if (DEBUG_LISTENER) {
            Log.d(TAG, "onPress    : " + KeyDetector.printableCode(key)
@@ -286,6 +295,9 @@ public class PointerTracker {
    // Note that we need primaryCode argument because the keyboard may in shifted state and the
    // primaryCode is different from {@link Key#mCode}.
    private void callListenerOnRelease(Key key, int primaryCode, boolean withSliding) {
        if (sGestureTracker.isInGesture()) {
            return;
        }
        final boolean ignoreModifierKey = mIgnoreModifierKey && key.isModifier();
        if (DEBUG_LISTENER) {
            Log.d(TAG, "onRelease  : " + Keyboard.printableCode(primaryCode)
@@ -386,7 +398,7 @@ public class PointerTracker {
            return;
        }

        if (!key.noKeyPreview()) {
        if (!key.noKeyPreview() && !sGestureTracker.isInGesture()) {
            mDrawingProxy.showKeyPreview(this);
        }
        updatePressKeyGraphics(key);
@@ -504,8 +516,8 @@ public class PointerTracker {
        }

        final PointerTrackerQueue queue = sPointerTrackerQueue;
        if (queue != null) {
        final Key key = getKeyOn(x, y);
        if (queue != null) {
            if (key != null && key.isModifier()) {
                // Before processing a down event of modifier key, all pointers already being
                // tracked should be released.
@@ -514,6 +526,9 @@ public class PointerTracker {
            queue.add(this);
        }
        onDownEventInternal(x, y, eventTime);
        if (queue != null && queue.size() == 1) {
            sGestureTracker.onDownEvent(this, x, y, eventTime, key);
        }
    }

    private void onDownEventInternal(int x, int y, long eventTime) {
@@ -554,10 +569,34 @@ public class PointerTracker {
        if (mKeyAlreadyProcessed)
            return;

        if (me != null) {
            // Add historical points to gesture path.
            final int pointerIndex = me.findPointerIndex(mPointerId);
            final int historicalSize = me.getHistorySize();
            for (int h = 0; h < historicalSize; h++) {
                final int historicalX = (int)me.getHistoricalX(pointerIndex, h);
                final int historicalY = (int)me.getHistoricalY(pointerIndex, h);
                final long historicalTime = me.getHistoricalEventTime(h);
                sGestureTracker.onMoveEvent(this, historicalX, historicalY, historicalTime,
                        true /* isHistorical */, null);
            }
        }

        final int lastX = mLastX;
        final int lastY = mLastY;
        final Key oldKey = mCurrentKey;
        Key key = onMoveKey(x, y);

        // Register move event on gesture tracker.
        sGestureTracker.onMoveEvent(this, x, y, eventTime, false, key);
        if (sGestureTracker.isInGesture()) {
            mIgnoreModifierKey = true;
            mTimerProxy.cancelLongPressTimer();
            mIsInSlidingKeyInput = true;
            mCurrentKey = null;
            setReleasedKeyGraphics(oldKey);
        }

        if (key != null) {
            if (oldKey == null) {
                // The pointer has been slid in to the new key, but the finger was not on any keys.
@@ -607,7 +646,7 @@ public class PointerTracker {
                        if (ProductionFlag.IS_EXPERIMENTAL) {
                            ResearchLogger.pointerTracker_onMoveEvent(x, y, lastX, lastY);
                        }
                        onUpEventInternal();
                        onUpEventInternal(x, y, eventTime);
                        onDownEventInternal(x, y, eventTime);
                    } else {
                        // HACK: If there are currently multiple touches, register the key even if
@@ -617,7 +656,7 @@ public class PointerTracker {
                        // this hack.
                        if (me != null && me.getPointerCount() > 1
                                && !sPointerTrackerQueue.hasModifierKeyOlderThan(this)) {
                            onUpEventInternal();
                            onUpEventInternal(x, y, eventTime);
                        }
                        mKeyAlreadyProcessed = true;
                        setReleasedKeyGraphics(oldKey);
@@ -647,6 +686,7 @@ public class PointerTracker {

        final PointerTrackerQueue queue = sPointerTrackerQueue;
        if (queue != null) {
            if (!sGestureTracker.isInGesture()) {
                if (mCurrentKey != null && mCurrentKey.isModifier()) {
                    // Before processing an up event of modifier key, all pointers already being
                    // tracked should be released.
@@ -654,9 +694,10 @@ public class PointerTracker {
                } else {
                    queue.releaseAllPointersOlderThan(this, eventTime);
                }
            }
            queue.remove(this);
        }
        onUpEventInternal();
        onUpEventInternal(x, y, eventTime);
    }

    // Let this pointer tracker know that one of newer-than-this pointer trackers got an up event.
@@ -665,11 +706,11 @@ public class PointerTracker {
    public void onPhantomUpEvent(int x, int y, long eventTime) {
        if (DEBUG_EVENT)
            printTouchEvent("onPhntEvent:", x, y, eventTime);
        onUpEventInternal();
        onUpEventInternal(x, y, eventTime);
        mKeyAlreadyProcessed = true;
    }

    private void onUpEventInternal() {
    private void onUpEventInternal(int x, int y, long eventTime) {
        mTimerProxy.cancelKeyTimers();
        mIsInSlidingKeyInput = false;
        // Release the last pressed key.
@@ -678,6 +719,24 @@ public class PointerTracker {
            mDrawingProxy.dismissMoreKeysPanel();
            mIsShowingMoreKeysPanel = false;
        }

        if (sGestureTracker.isInGesture()) {
            // Register up event on gesture tracker.
            sGestureTracker.onUpEvent(this, x, y, eventTime);
            if (!sPointerTrackerQueue.isAnyInSlidingKeyInput()) {
                // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code.
                sGestureTracker.endBatchInput();
            }
            if (mCurrentKey != null) {
                callListenerOnRelease(mCurrentKey, mCurrentKey.mCode, true);
            }
            mCurrentKey = null;
            return;
        } else {
            // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code.
            sGestureTracker.endBatchInput();
        }

        if (mKeyAlreadyProcessed)
            return;
        if (mCurrentKey != null && !mCurrentKey.isRepeatable()) {
@@ -689,6 +748,8 @@ public class PointerTracker {
        onLongPressed();
        onDownEvent(x, y, SystemClock.uptimeMillis(), handler);
        mIsShowingMoreKeysPanel = true;
        // TODO: Calls to beginBatchInput() is missing in this class. Reorganize the code.
        sGestureTracker.abortBatchInput();
    }

    public void onLongPressed() {
@@ -723,7 +784,7 @@ public class PointerTracker {
    }

    private void startRepeatKey(Key key) {
        if (key != null && key.isRepeatable()) {
        if (key != null && key.isRepeatable() && !sGestureTracker.isInGesture()) {
            onRegisterKey(key);
            mTimerProxy.startKeyRepeatTimer(this);
        }
@@ -753,7 +814,7 @@ public class PointerTracker {
    }

    private void startLongPressTimer(Key key) {
        if (key != null && key.isLongPressEnabled()) {
        if (key != null && key.isLongPressEnabled() && !sGestureTracker.isInGesture()) {
            mTimerProxy.startLongPressTimer(this);
        }
    }
+325 −0
Original line number Diff line number Diff line
/*
 * 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.util.Log;
import android.util.SparseArray;

import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.latin.InputPointers;
import com.android.inputmethod.latin.SuggestedWords;

// TODO: Remove this class by consolidating with PointerTracker
public class GestureTracker {
    private static final String TAG = GestureTracker.class.getSimpleName();
    private static final boolean DEBUG_LISTENER = false;

    // TODO: There should be an option to turn on/off the gesture input.
    private static final boolean GESTURE_ON = true;

    private static final GestureTracker sInstance = new GestureTracker();

    private static final int MIN_RECOGNITION_TIME = 100;
    private static final int MIN_GESTURE_DURATION = 200;

    private static final float GESTURE_RECOG_SPEED_THRESHOLD = 0.4f;
    private static final float SQUARED_GESTURE_RECOG_SPEED_THRESHOLD =
            GESTURE_RECOG_SPEED_THRESHOLD * GESTURE_RECOG_SPEED_THRESHOLD;
    private static final float GESTURE_RECOG_CURVATURE_THRESHOLD = (float) (Math.PI / 4);

    private boolean mIsAlphabetKeyboard;
    private boolean mIsPossibleGesture = false;
    private boolean mInGesture = false;

    private KeyboardActionListener mListener;
    private SuggestedWords mSuggestions;

    private final SparseArray<GestureStroke> mGestureStrokes = new SparseArray<GestureStroke>();

    private int mLastRecognitionPointSize = 0;
    private long mLastRecognitionTime = 0;

    public static void init(KeyboardActionListener listner) {
        sInstance.mListener = listner;
    }

    public static GestureTracker getInstance() {
        return sInstance;
    }

    private GestureTracker() {
    }

    public void setKeyboard(Keyboard keyboard) {
        mIsAlphabetKeyboard = keyboard.mId.isAlphabetKeyboard();
        GestureStroke.setGestureSampleLength(keyboard.mMostCommonKeyWidth / 2,
                keyboard.mMostCommonKeyHeight / 6);
    }

    private void startBatchInput() {
        if (DEBUG_LISTENER) {
            Log.d(TAG, "onStartBatchInput");
        }
        mInGesture = true;
        mListener.onStartBatchInput();
        mSuggestions = null;
    }

    // TODO: The corresponding startBatchInput() is a private method. Reorganize the code.
    public void endBatchInput() {
        if (isInGesture() && mSuggestions != null && mSuggestions.size() > 0) {
            final CharSequence text = mSuggestions.getWord(0);
            if (DEBUG_LISTENER) {
                Log.d(TAG, "onEndBatchInput: text=" + text);
            }
            mListener.onEndBatchInput(text);
        }
        mInGesture = false;
        clearBatchInputPoints();
    }

    public void abortBatchInput() {
        mIsPossibleGesture = false;
        mInGesture = false;
    }

    public boolean isInGesture() {
        return mInGesture;
    }

    public void onDownEvent(PointerTracker tracker, int x, int y, long eventTime, Key key) {
        mIsPossibleGesture = false;
        if (GESTURE_ON && mIsAlphabetKeyboard && key != null && !key.isModifier()) {
            mIsPossibleGesture = true;
            addPointToStroke(x, y, 0, tracker.mPointerId, false);
        }
    }

    public void onMoveEvent(PointerTracker tracker, int x, int y, long eventTime,
            boolean isHistorical, Key key) {
        final int gestureTime = (int)(eventTime - tracker.getDownTime());
        if (GESTURE_ON && mIsPossibleGesture) {
            final GestureStroke stroke = addPointToStroke(x, y, gestureTime, tracker.mPointerId,
                    isHistorical);
            if (!isInGesture() && stroke.isStartOfAGesture(gestureTime)) {
                startBatchInput();
            }
        }

        if (key != null && isInGesture()) {
            final InputPointers batchPoints = getIncrementalBatchPoints();
            if (updateBatchInputRecognitionState(eventTime, batchPoints.getPointerSize())) {
                if (DEBUG_LISTENER) {
                    Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize());
                }
                mSuggestions = mListener.onUpdateBatchInput(batchPoints);
            }
        }
    }

    public void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) {
        if (isInGesture()) {
            final InputPointers batchPoints = getAllBatchPoints();
            if (DEBUG_LISTENER) {
                Log.d(TAG, "onUpdateBatchInput: batchPoints=" + batchPoints.getPointerSize());
            }
            mSuggestions = mListener.onUpdateBatchInput(batchPoints);
        }
    }

    private GestureStroke addPointToStroke(int x, int y, int time, int pointerId,
            boolean isHistorical) {
        GestureStroke stroke = mGestureStrokes.get(pointerId);
        if (stroke == null) {
            stroke = new GestureStroke(pointerId);
            mGestureStrokes.put(pointerId, stroke);
        }
        stroke.addPoint(x, y, time, isHistorical);
        return stroke;
    }

    // The working and return object of the following methods, {@link #getIncrementalBatchPoints()}
    // and {@link #getAllBatchPoints()}.
    private final InputPointers mAggregatedPointers = new InputPointers();

    private InputPointers getIncrementalBatchPoints() {
        final InputPointers pointers = mAggregatedPointers;
        pointers.reset();
        final int strokeSize = mGestureStrokes.size();
        for (int index = 0; index < strokeSize; index++) {
            final GestureStroke stroke = mGestureStrokes.valueAt(index);
            stroke.appendIncrementalBatchPoints(pointers);
        }
        return pointers;
    }

    private InputPointers getAllBatchPoints() {
        final InputPointers pointers = mAggregatedPointers;
        pointers.reset();
        final int strokeSize = mGestureStrokes.size();
        for (int index = 0; index < strokeSize; index++) {
            final GestureStroke stroke = mGestureStrokes.valueAt(index);
            stroke.appendAllBatchPoints(pointers);
        }
        return pointers;
    }

    private void clearBatchInputPoints() {
        final int strokeSize = mGestureStrokes.size();
        for (int index = 0; index < strokeSize; index++) {
            final GestureStroke stroke = mGestureStrokes.valueAt(index);
            stroke.reset();
        }
        mLastRecognitionPointSize = 0;
        mLastRecognitionTime = 0;
    }

    private boolean updateBatchInputRecognitionState(long eventTime, int size) {
        if (size > mLastRecognitionPointSize
                && eventTime > mLastRecognitionTime + MIN_RECOGNITION_TIME) {
            mLastRecognitionPointSize = size;
            mLastRecognitionTime = eventTime;
            return true;
        }
        return false;
    }

    private static class GestureStroke {
        private final int mPointerId;
        private final InputPointers mInputPointers = new InputPointers();
        private float mLength;
        private float mAngle;
        private int mIncrementalRecognitionPoint;
        private boolean mHasSharpCorner;
        private long mLastPointTime;
        private int mLastPointX;
        private int mLastPointY;

        private static int sMinGestureLength;
        private static int sSquaredGestureSampleLength;

        private static final float DOUBLE_PI = (float)(2 * Math.PI);

        public static void setGestureSampleLength(final int minGestureLength,
                final int sampleLength) {
            sMinGestureLength = minGestureLength;
            sSquaredGestureSampleLength = sampleLength * sampleLength;
        }

        public GestureStroke(int pointerId) {
            mPointerId = pointerId;
            reset();
        }

        public boolean isStartOfAGesture(int downDuration) {
            return downDuration > MIN_GESTURE_DURATION / 2  && mLength > sMinGestureLength / 2;
        }

        public void reset() {
            mLength = 0;
            mAngle = 0;
            mIncrementalRecognitionPoint = 0;
            mHasSharpCorner = false;
            mLastPointTime = 0;
            mInputPointers.reset();
        }

        private void updateLastPoint(final int x, final int y, final int time) {
            mLastPointTime = time;
            mLastPointX = x;
            mLastPointY = y;
        }

        public void addPoint(final int x, final int y, final int time, final boolean isHistorical) {
            final int size = mInputPointers.getPointerSize();
            if (size == 0) {
                mInputPointers.addPointer(x, y, mPointerId, time);
                if (!isHistorical) {
                    updateLastPoint(x, y, time);
                }
                return;
            }

            final int[] xCoords = mInputPointers.getXCoordinates();
            final int[] yCoords = mInputPointers.getYCoordinates();
            final int lastX = xCoords[size - 1];
            final int lastY = yCoords[size - 1];
            final float dist = squaredDistance(lastX, lastY, x, y);
            if (dist > sSquaredGestureSampleLength) {
                mInputPointers.addPointer(x, y, mPointerId, time);
                mLength += dist;
                final float angle = angle(lastX, lastY, x, y);
                if (size > 1) {
                    float curvature = getAngleDiff(angle, mAngle);
                    if (curvature > GESTURE_RECOG_CURVATURE_THRESHOLD) {
                        if (size > mIncrementalRecognitionPoint) {
                            mIncrementalRecognitionPoint = size;
                        }
                        mHasSharpCorner = true;
                    }
                    if (!mHasSharpCorner) {
                        mIncrementalRecognitionPoint = size;
                    }
                }
                mAngle = angle;
            }

            if (!isHistorical) {
                final int duration = (int)(time - mLastPointTime);
                if (mLastPointTime != 0 && duration > 0) {
                    final int squaredDuration = duration * duration;
                    final float squaredSpeed =
                            squaredDistance(mLastPointX, mLastPointY, x, y) / squaredDuration;
                    if (squaredSpeed < SQUARED_GESTURE_RECOG_SPEED_THRESHOLD) {
                        mIncrementalRecognitionPoint = size;
                    }
                }
                updateLastPoint(x, y, time);
            }
        }

        private float getAngleDiff(float a1, float a2) {
            final float diff = Math.abs(a1 - a2);
            if (diff > Math.PI) {
                return DOUBLE_PI - diff;
            }
            return diff;
        }

        public void appendAllBatchPoints(InputPointers out) {
            out.append(mInputPointers, 0, mInputPointers.getPointerSize());
        }

        public void appendIncrementalBatchPoints(InputPointers out) {
            out.append(mInputPointers, 0, mIncrementalRecognitionPoint);
        }
    }

    static float squaredDistance(int p1x, int p1y, int p2x, int p2y) {
        final float dx = p1x - p2x;
        final float dy = p1y - p2y;
        return dx * dx + dy * dy;
    }

    static float angle(int p1x, int p1y, int p2x, int p2y) {
        final int dx = p1x - p2x;
        final int dy = p1y - p2y;
        if (dx == 0 && dy == 0) return 0;
        return (float)Math.atan2(dy, dx);
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -31,6 +31,10 @@ public class PointerTrackerQueue {
    // TODO: Use ring buffer instead of {@link LinkedList}.
    private final LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();

    public int size() {
        return mQueue.size();
    }

    public synchronized void add(PointerTracker tracker) {
        mQueue.add(tracker);
    }
+28 −9

File changed.

Preview size limit exceeded, changes collapsed.

Loading