Added text navigation gestures for keyboard touch exploration.

Bug: 4905427
Change-Id: I9b44d65e4503e46ce71322a3c325c55d188e34a0
This commit is contained in:
Alan Viverette 2011-06-22 17:11:07 -07:00
parent 7e7244873a
commit 87d7929d14
4 changed files with 301 additions and 3 deletions

View file

@ -16,11 +16,15 @@
package com.android.inputmethod.accessibility; package com.android.inputmethod.accessibility;
import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService; import android.inputmethodservice.InputMethodService;
import android.media.AudioManager;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.os.Vibrator;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.ExtractedTextRequest;
@ -38,8 +42,14 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi
*/ */
private static final long DELAY_NO_HOVER_SELECTION = 250; 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 AccessibilityHandler mAccessibilityHandler;
private static class AccessibilityHandler private static class AccessibilityHandler
@ -84,6 +94,8 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi
private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) { private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
mInputMethod = inputMethod; mInputMethod = inputMethod;
mVibrator = (Vibrator) inputMethod.getSystemService(Context.VIBRATOR_SERVICE);
mAudioManager = (AudioManager) inputMethod.getSystemService(Context.AUDIO_SERVICE);
mAccessibilityHandler = new AccessibilityHandler(this, inputMethod.getMainLooper()); mAccessibilityHandler = new AccessibilityHandler(this, inputMethod.getMainLooper());
} }
@ -106,6 +118,35 @@ public class AccessibleInputMethodServiceProxy implements AccessibleKeyboardActi
mAccessibilityHandler.postNoHoverSelection(); 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 * When Accessibility is turned on, notifies the user that they are not
* currently hovering above a key. By default this will speak the currently * currently hovering above a key. By default this will speak the currently

View file

@ -34,4 +34,15 @@ public interface AccessibleKeyboardActionListener {
* @param primaryCode the code of the key that was hovered over * @param primaryCode the code of the key that was hovered over
*/ */
public void onHoverExit(int primaryCode); public void onHoverExit(int primaryCode);
/**
* @param direction the direction of the flick gesture, one of
* <ul>
* <li>{@link FlickGestureDetector#FLICK_UP}
* <li>{@link FlickGestureDetector#FLICK_DOWN}
* <li>{@link FlickGestureDetector#FLICK_LEFT}
* <li>{@link FlickGestureDetector#FLICK_RIGHT}
* </ul>
*/
public void onFlickGesture(int direction);
} }

View file

@ -42,6 +42,7 @@ public class AccessibleKeyboardViewProxy {
private int mScaledEdgeSlop; private int mScaledEdgeSlop;
private KeyboardView mView; private KeyboardView mView;
private AccessibleKeyboardActionListener mListener; private AccessibleKeyboardActionListener mListener;
private FlickGestureDetector mGestureDetector;
private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY; private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
private int mLastX = -1; private int mLastX = -1;
@ -71,6 +72,7 @@ public class AccessibleKeyboardViewProxy {
paint.setAntiAlias(true); paint.setAntiAlias(true);
paint.setColor(Color.YELLOW); paint.setColor(Color.YELLOW);
mGestureDetector = new KeyboardFlickGestureDetector(context);
mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop(); mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
} }
@ -110,7 +112,10 @@ public class AccessibleKeyboardViewProxy {
* @return {@code true} if the event is handled * @return {@code true} if the event is handled
*/ */
public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) { 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) { public boolean dispatchTouchEvent(MotionEvent event) {
@ -128,7 +133,7 @@ public class AccessibleKeyboardViewProxy {
* @param event The touch exploration hover event. * @param event The touch exploration hover event.
* @return {@code true} if the event was handled * @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 x = (int) event.getX();
final int y = (int) event.getY(); final int y = (int) event.getY();
@ -198,4 +203,18 @@ public class AccessibleKeyboardViewProxy {
tracker.onDownEvent(x, y, eventTime, null); tracker.onDownEvent(x, y, eventTime, null);
tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, 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;
}
}
} }

View file

@ -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.
* <p>
* A flick gesture is defined as a stream of hover events with the following
* properties:
* <ul>
* <li>Begins with a {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event
* <li>Contains any number of {@link MotionEventCompatUtils#ACTION_HOVER_MOVE}
* events
* <li>Ends with a {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event
* <li>Maximum duration of 250 milliseconds
* <li>Minimum distance between enter and exit points must be at least equal to
* scaled double tap slop (see
* {@link ViewConfiguration#getScaledDoubleTapSlop()})
* </ul>
* <p>
* 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<FlickGestureDetector> {
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:
* <ul>
* <li>{@link #FLICK_UP}
* <li>{@link #FLICK_DOWN}
* <li>{@link #FLICK_LEFT}
* <li>{@link #FLICK_RIGHT}
* </ul>
* @return {@code true} if the flick event was handled.
*/
public abstract boolean onFlick(MotionEvent e1, MotionEvent e2, int direction);
}