From 87d7929d142f7c5f1937e12d6fd32a43ab00740e Mon Sep 17 00:00:00 2001 From: Alan Viverette Date: Wed, 22 Jun 2011 17:11:07 -0700 Subject: [PATCH] Added text navigation gestures for keyboard touch exploration. Bug: 4905427 Change-Id: I9b44d65e4503e46ce71322a3c325c55d188e34a0 --- .../AccessibleInputMethodServiceProxy.java | 43 +++- .../AccessibleKeyboardActionListener.java | 11 + .../AccessibleKeyboardViewProxy.java | 23 +- .../accessibility/FlickGestureDetector.java | 227 ++++++++++++++++++ 4 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java index 7199550a9..89adc15f2 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java @@ -16,11 +16,15 @@ package com.android.inputmethod.accessibility; +import android.content.Context; import android.content.SharedPreferences; import android.inputmethodservice.InputMethodService; +import android.media.AudioManager; import android.os.Looper; import android.os.Message; +import android.os.Vibrator; import android.text.TextUtils; +import android.view.KeyEvent; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -38,8 +42,14 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi */ private static final long DELAY_NO_HOVER_SELECTION = 250; - private InputMethodService mInputMethod; + /** + * Duration of the key click vibration in milliseconds. + */ + private static final long VIBRATE_KEY_CLICK = 50; + private InputMethodService mInputMethod; + private Vibrator mVibrator; + private AudioManager mAudioManager; private AccessibilityHandler mAccessibilityHandler; private static class AccessibilityHandler @@ -84,6 +94,8 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) { mInputMethod = inputMethod; + mVibrator = (Vibrator) inputMethod.getSystemService(Context.VIBRATOR_SERVICE); + mAudioManager = (AudioManager) inputMethod.getSystemService(Context.AUDIO_SERVICE); mAccessibilityHandler = new AccessibilityHandler(this, inputMethod.getMainLooper()); } @@ -106,6 +118,35 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi mAccessibilityHandler.postNoHoverSelection(); } + /** + * Handle flick gestures by mapping them to directional pad keys. + */ + @Override + public void onFlickGesture(int direction) { + final int keyEventCode; + + switch (direction) { + case FlickGestureDetector.FLICK_LEFT: + sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_LEFT); + break; + case FlickGestureDetector.FLICK_RIGHT: + sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_RIGHT); + break; + } + } + + /** + * Provide haptic feedback and send the specified keyCode to the input + * connection as a pair of down/up events. + * + * @param keyCode + */ + private void sendDownUpKeyEvents(int keyCode) { + mVibrator.vibrate(VIBRATE_KEY_CLICK); + mAudioManager.playSoundEffect(AudioManager.FX_KEY_CLICK); + mInputMethod.sendDownUpKeyEvents(keyCode); + } + /** * When Accessibility is turned on, notifies the user that they are not * currently hovering above a key. By default this will speak the currently diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java index 12c59d0fc..c1e92bec8 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java @@ -34,4 +34,15 @@ public interface AccessibleKeyboardActionListener { * @param primaryCode the code of the key that was hovered over */ public void onHoverExit(int primaryCode); + + /** + * @param direction the direction of the flick gesture, one of + * + */ + public void onFlickGesture(int direction); } diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java index 96f7fc9f2..a87ff9891 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -42,6 +42,7 @@ public class AccessibleKeyboardViewProxy { private int mScaledEdgeSlop; private KeyboardView mView; private AccessibleKeyboardActionListener mListener; + private FlickGestureDetector mGestureDetector; private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY; private int mLastX = -1; @@ -71,6 +72,7 @@ public class AccessibleKeyboardViewProxy { paint.setAntiAlias(true); paint.setColor(Color.YELLOW); + mGestureDetector = new KeyboardFlickGestureDetector(context); mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop(); } @@ -110,7 +112,10 @@ public class AccessibleKeyboardViewProxy { * @return {@code true} if the event is handled */ public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) { - return onTouchExplorationEvent(event, tracker); + if (mGestureDetector.onHoverEvent(event, this, tracker)) + return true; + + return onHoverEventInternal(event, tracker); } public boolean dispatchTouchEvent(MotionEvent event) { @@ -128,7 +133,7 @@ public class AccessibleKeyboardViewProxy { * @param event The touch exploration hover event. * @return {@code true} if the event was handled */ - private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) { + /*package*/ boolean onHoverEventInternal(MotionEvent event, PointerTracker tracker) { final int x = (int) event.getX(); final int y = (int) event.getY(); @@ -198,4 +203,18 @@ public class AccessibleKeyboardViewProxy { tracker.onDownEvent(x, y, eventTime, null); tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, null); } + + private class KeyboardFlickGestureDetector extends FlickGestureDetector { + public KeyboardFlickGestureDetector(Context context) { + super(context); + } + + @Override + public boolean onFlick(MotionEvent e1, MotionEvent e2, int direction) { + if (mListener != null) { + mListener.onFlickGesture(direction); + } + return true; + } + } } diff --git a/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java b/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java new file mode 100644 index 000000000..9d99e3131 --- /dev/null +++ b/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2011 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.accessibility; + +import android.content.Context; +import android.os.Message; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.inputmethod.compat.MotionEventCompatUtils; +import com.android.inputmethod.keyboard.PointerTracker; +import com.android.inputmethod.latin.StaticInnerHandlerWrapper; + +/** + * Detects flick gestures within a stream of hover events. + *

+ * A flick gesture is defined as a stream of hover events with the following + * properties: + *

+ *

+ * Initial enter events are intercepted and cached until the stream fails to + * satisfy the constraints defined above, at which point the cached enter event + * is sent to its source {@link AccessibleKeyboardViewProxy} and subsequent move + * and exit events are ignored. + */ +public abstract class FlickGestureDetector { + public static final int FLICK_UP = 0; + public static final int FLICK_RIGHT = 1; + public static final int FLICK_LEFT = 2; + public static final int FLICK_DOWN = 3; + + private final FlickHandler mFlickHandler; + private final int mFlickRadiusSquare; + + private AccessibleKeyboardViewProxy mCachedView; + private PointerTracker mCachedTracker; + private MotionEvent mCachedHoverEnter; + + private static class FlickHandler extends StaticInnerHandlerWrapper { + private static final int MSG_FLICK_TIMEOUT = 1; + + /** The maximum duration of a flick gesture in milliseconds. */ + private static final int DELAY_FLICK_TIMEOUT = 250; + + public FlickHandler(FlickGestureDetector outerInstance) { + super(outerInstance); + } + + @Override + public void handleMessage(Message msg) { + final FlickGestureDetector gestureDetector = getOuterInstance(); + + switch (msg.what) { + case MSG_FLICK_TIMEOUT: + gestureDetector.clearFlick(true); + } + } + + public void startFlickTimeout() { + cancelFlickTimeout(); + sendEmptyMessageDelayed(MSG_FLICK_TIMEOUT, DELAY_FLICK_TIMEOUT); + } + + public void cancelFlickTimeout() { + removeMessages(MSG_FLICK_TIMEOUT); + } + } + + /** + * Creates a new flick gesture detector. + * + * @param context The parent context. + */ + public FlickGestureDetector(Context context) { + final int doubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop(); + + mFlickHandler = new FlickHandler(this); + mFlickRadiusSquare = doubleTapSlop * doubleTapSlop; + } + + /** + * Processes motion events to detect flick gestures. + * + * @param event The current event. + * @param view The source of the event. + * @param tracker A pointer tracker for the event. + * @return {@code true} if the event was handled. + */ + public boolean onHoverEvent(MotionEvent event, AccessibleKeyboardViewProxy view, + PointerTracker tracker) { + // Always cache and consume the first hover event. + if (event.getAction() == MotionEventCompatUtils.ACTION_HOVER_ENTER) { + mCachedView = view; + mCachedTracker = tracker; + mCachedHoverEnter = MotionEvent.obtain(event); + mFlickHandler.startFlickTimeout(); + return true; + } + + // Stop if the event has already been canceled. + if (mCachedHoverEnter == null) { + return false; + } + + final float distanceSquare = calculateDistanceSquare(mCachedHoverEnter, event); + final long timeout = event.getEventTime() - mCachedHoverEnter.getEventTime(); + + switch (event.getAction()) { + case MotionEventCompatUtils.ACTION_HOVER_MOVE: + // Consume all valid move events before timeout. + return true; + case MotionEventCompatUtils.ACTION_HOVER_EXIT: + // Ignore exit events outside the flick radius. + if (distanceSquare < mFlickRadiusSquare) { + clearFlick(true); + return false; + } else { + return dispatchFlick(mCachedHoverEnter, event); + } + default: + return false; + } + } + + /** + * Clears the cached flick information and optionally forwards the event to + * the source view's internal hover event handler. + * + * @param sendCachedEvent Set to {@code true} to forward the hover event to + * the source view. + */ + private void clearFlick(boolean sendCachedEvent) { + mFlickHandler.cancelFlickTimeout(); + + if (mCachedHoverEnter != null) { + if (sendCachedEvent) { + mCachedView.onHoverEventInternal(mCachedHoverEnter, mCachedTracker); + } + mCachedHoverEnter.recycle(); + mCachedHoverEnter = null; + } + + mCachedTracker = null; + mCachedView = null; + } + + /** + * Computes the direction of a flick gesture and forwards it to + * {@link #onFlick(MotionEvent, MotionEvent, int)} for handling. + * + * @param e1 The {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event + * where the flick started. + * @param e2 The {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event + * where the flick ended. + * @return {@code true} if the flick event was handled. + */ + private boolean dispatchFlick(MotionEvent e1, MotionEvent e2) { + clearFlick(false); + + final float dX = e2.getX() - e1.getX(); + final float dY = e2.getY() - e1.getY(); + final int direction; + + if (dY > dX) { + if (dY > -dX) { + direction = FLICK_DOWN; + } else { + direction = FLICK_LEFT; + } + } else { + if (dY > -dX) { + direction = FLICK_RIGHT; + } else { + direction = FLICK_UP; + } + } + + return onFlick(e1, e2, direction); + } + + private float calculateDistanceSquare(MotionEvent e1, MotionEvent e2) { + final float dX = e2.getX() - e1.getX(); + final float dY = e2.getY() - e1.getY(); + return (dX * dX) + (dY * dY); + } + + /** + * Handles a detected flick gesture. + * + * @param e1 The {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event + * where the flick started. + * @param e2 The {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event + * where the flick ended. + * @param direction The direction of the flick event, one of: + *

+ * @return {@code true} if the flick event was handled. + */ + public abstract boolean onFlick(MotionEvent e1, MotionEvent e2, int direction); +}