Skip to content
Snippets Groups Projects
Commit 44f24b73 authored by Ken Wakasa's avatar Ken Wakasa Committed by Android (Google) Code Review
Browse files

Merge "Added text navigation gestures for keyboard touch exploration."

parents 71fbdc1a 87d7929d
No related branches found
No related tags found
No related merge requests found
......@@ -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
......
......@@ -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
* <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);
}
......@@ -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;
}
}
}
/*
* 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);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment