mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Refactor keyboard accessibility to use ExploreByTouchHelper
This commit is contained in:
parent
1f9cbdee09
commit
0e470f4c39
@ -17,22 +17,32 @@
|
||||
package org.futo.inputmethod.accessibility;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.AccessibilityDelegateCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
|
||||
import androidx.customview.widget.ExploreByTouchHelper;
|
||||
|
||||
import org.futo.inputmethod.keyboard.Key;
|
||||
import org.futo.inputmethod.keyboard.KeyDetector;
|
||||
import org.futo.inputmethod.keyboard.Keyboard;
|
||||
import org.futo.inputmethod.keyboard.KeyboardView;
|
||||
import org.futo.inputmethod.latin.common.Constants;
|
||||
import org.futo.inputmethod.latin.settings.Settings;
|
||||
import org.futo.inputmethod.latin.settings.SettingsValues;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class represents a delegate that can be registered in a class that extends
|
||||
@ -40,25 +50,23 @@ import org.futo.inputmethod.keyboard.KeyboardView;
|
||||
*
|
||||
* To implement accessibility mode, the target keyboard view has to:<p>
|
||||
* - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view.
|
||||
* - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}.
|
||||
*
|
||||
* @param <KV> The keyboard view class type.
|
||||
*/
|
||||
public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
|
||||
extends AccessibilityDelegateCompat {
|
||||
extends ExploreByTouchHelper {
|
||||
private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName();
|
||||
protected static final boolean DEBUG_HOVER = false;
|
||||
|
||||
protected final KV mKeyboardView;
|
||||
protected final KeyDetector mKeyDetector;
|
||||
private Keyboard mKeyboard;
|
||||
private KeyboardAccessibilityNodeProvider<KV> mAccessibilityNodeProvider;
|
||||
private Key mLastHoverKey;
|
||||
|
||||
public static final int HOVER_EVENT_POINTER_ID = 0;
|
||||
|
||||
public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
|
||||
super();
|
||||
super(keyboardView);
|
||||
mKeyboardView = keyboardView;
|
||||
mKeyDetector = keyDetector;
|
||||
|
||||
@ -77,9 +85,6 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
}
|
||||
if (mAccessibilityNodeProvider != null) {
|
||||
mAccessibilityNodeProvider.setKeyboard(keyboard);
|
||||
}
|
||||
mKeyboard = keyboard;
|
||||
}
|
||||
|
||||
@ -126,128 +131,57 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
|
||||
* version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
|
||||
* node hierarchy provider.
|
||||
*
|
||||
* @param host The host view for the provider.
|
||||
* @return The accessibility node provider for the current keyboard.
|
||||
*/
|
||||
@Override
|
||||
public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(final View host) {
|
||||
return getAccessibilityNodeProvider();
|
||||
protected int getVirtualViewAt(float x, float y) {
|
||||
Key k = mKeyDetector.detectHitKey((int)x, (int)y);
|
||||
if(k == null) {
|
||||
return HOST_ID;
|
||||
}
|
||||
|
||||
return getVirtualViewIdOf(k);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A lazily-instantiated node provider for this view delegate.
|
||||
*/
|
||||
protected AccessibilityNodeProviderCompat getAccessibilityNodeProvider() {
|
||||
// Instantiate the provide only when requested. Since the system
|
||||
// will call this method multiple times it is a good practice to
|
||||
// cache the provider instance.
|
||||
if (mAccessibilityNodeProvider == null) {
|
||||
mAccessibilityNodeProvider =
|
||||
new KeyboardAccessibilityNodeProvider<>(mKeyboardView, this);
|
||||
@Override
|
||||
protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
final int size = sortedKeys.size();
|
||||
for (int index = 0; index < size; index++) {
|
||||
virtualViewIds.add(index);
|
||||
}
|
||||
return mAccessibilityNodeProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a key that a hover event is on.
|
||||
*
|
||||
* @param event The hover event.
|
||||
* @return key The key that the <code>event</code> is on.
|
||||
*/
|
||||
protected final Key getHoverKeyOf(final MotionEvent event) {
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
return mKeyDetector.detectHitKey(x, y);
|
||||
@Override
|
||||
protected void onPopulateNodeForVirtualView(int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) {
|
||||
Key k = getKeyOf(virtualViewId);
|
||||
if(k == null) return;
|
||||
|
||||
String description = getKeyDescription(k);
|
||||
|
||||
node.setContentDescription(description);
|
||||
node.setBoundsInParent(k.getHitBox());
|
||||
|
||||
node.setFocusable(true);
|
||||
node.setScreenReaderFocusable(true);
|
||||
|
||||
if(k.isActionKey() || k.getCode() == Constants.CODE_SWITCH_ALPHA_SYMBOL || k.getCode() == Constants.CODE_EMOJI || k.getCode() == Constants.CODE_SYMBOL_SHIFT) {
|
||||
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||
node.setClickable(true);
|
||||
} else {
|
||||
node.setTextEntryKey(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
|
||||
*
|
||||
* @param event The hover event.
|
||||
* @return {@code true} if the event is handled.
|
||||
*/
|
||||
public boolean onHoverEvent(final MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_HOVER_ENTER:
|
||||
onHoverEnter(event);
|
||||
break;
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
onHoverMove(event);
|
||||
break;
|
||||
case MotionEvent.ACTION_HOVER_EXIT:
|
||||
onHoverExit(event);
|
||||
break;
|
||||
default:
|
||||
Log.w(getClass().getSimpleName(), "Unknown hover event: " + event);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
protected boolean onPerformActionForVirtualView(int virtualViewId, int action, @Nullable Bundle arguments) {
|
||||
Key k = getKeyOf(virtualViewId);
|
||||
if(k == null) return false;
|
||||
|
||||
/**
|
||||
* Process {@link MotionEvent#ACTION_HOVER_ENTER} event.
|
||||
*
|
||||
* @param event A hover enter event.
|
||||
*/
|
||||
protected void onHoverEnter(final MotionEvent event) {
|
||||
final Key key = getHoverKeyOf(event);
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnter: key=" + key);
|
||||
if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
|
||||
// Handle the click action for the virtual button
|
||||
performClickOn(k);
|
||||
return true;
|
||||
}
|
||||
if (key != null) {
|
||||
onHoverEnterTo(key);
|
||||
}
|
||||
setLastHoverKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link MotionEvent#ACTION_HOVER_MOVE} event.
|
||||
*
|
||||
* @param event A hover move event.
|
||||
*/
|
||||
protected void onHoverMove(final MotionEvent event) {
|
||||
final Key lastKey = getLastHoverKey();
|
||||
final Key key = getHoverKeyOf(event);
|
||||
if (key != lastKey) {
|
||||
if (lastKey != null) {
|
||||
onHoverExitFrom(lastKey);
|
||||
}
|
||||
if (key != null) {
|
||||
onHoverEnterTo(key);
|
||||
}
|
||||
}
|
||||
if (key != null) {
|
||||
onHoverMoveWithin(key);
|
||||
}
|
||||
setLastHoverKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {@link MotionEvent#ACTION_HOVER_EXIT} event.
|
||||
*
|
||||
* @param event A hover exit event.
|
||||
*/
|
||||
protected void onHoverExit(final MotionEvent event) {
|
||||
final Key lastKey = getLastHoverKey();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
|
||||
}
|
||||
if (lastKey != null) {
|
||||
onHoverExitFrom(lastKey);
|
||||
}
|
||||
final Key key = getHoverKeyOf(event);
|
||||
// Make sure we're not getting an EXIT event because the user slid
|
||||
// off the keyboard area, then force a key press.
|
||||
if (key != null) {
|
||||
onHoverExitFrom(key);
|
||||
}
|
||||
setLastHoverKey(null);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -279,43 +213,6 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
|
||||
touchEvent.recycle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hover enter event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected void onHoverEnterTo(final Key key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnterTo: key=" + key);
|
||||
}
|
||||
key.onPressed();
|
||||
mKeyboardView.invalidateKey(key);
|
||||
final KeyboardAccessibilityNodeProvider<KV> provider = mAccessibilityNodeProvider;
|
||||
provider.onHoverEnterTo(key);
|
||||
provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a hover move event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected void onHoverMoveWithin(final Key key) { }
|
||||
|
||||
/**
|
||||
* Handles a hover exit event on a key.
|
||||
*
|
||||
* @param key The currently hovered key.
|
||||
*/
|
||||
protected void onHoverExitFrom(final Key key) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExitFrom: key=" + key);
|
||||
}
|
||||
key.onReleased();
|
||||
mKeyboardView.invalidateKey(key);
|
||||
final KeyboardAccessibilityNodeProvider<KV> provider = mAccessibilityNodeProvider;
|
||||
provider.onHoverExitFrom(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform long click on a key.
|
||||
@ -325,4 +222,51 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
|
||||
public void performLongClickOn(final Key key) {
|
||||
// A extended class should override this method to implement long press.
|
||||
}
|
||||
|
||||
|
||||
public Key getKeyOf(final int virtualViewId) {
|
||||
if (mKeyboard == null) {
|
||||
return null;
|
||||
}
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
// Use a virtual view id as an index of the sorted keys list.
|
||||
if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) {
|
||||
return sortedKeys.get(virtualViewId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getVirtualViewIdOf(final Key key) {
|
||||
if (mKeyboard == null) {
|
||||
return View.NO_ID;
|
||||
}
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
final int size = sortedKeys.size();
|
||||
for (int index = 0; index < size; index++) {
|
||||
if (sortedKeys.get(index) == key) {
|
||||
// Use an index of the sorted keys list as a virtual view id.
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return View.NO_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context-specific description for a {@link Key}.
|
||||
*
|
||||
* @param key The key to describe.
|
||||
* @return The context-specific description of the key.
|
||||
*/
|
||||
public String getKeyDescription(final Key key) {
|
||||
final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo;
|
||||
final boolean shouldObscure = AccessibilityUtils.getInstance().shouldObscureInput(editorInfo);
|
||||
final SettingsValues currentSettings = Settings.getInstance().getCurrent();
|
||||
final String keyCodeDescription = KeyCodeDescriptionMapper.getInstance().getDescriptionForKey(
|
||||
mKeyboardView.getContext(), mKeyboard, key, shouldObscure);
|
||||
if (currentSettings.isWordSeparator(key.getCode())) {
|
||||
return AccessibilityUtils.getInstance().getAutoCorrectionDescription(
|
||||
keyCodeDescription, shouldObscure);
|
||||
}
|
||||
return keyCodeDescription;
|
||||
}
|
||||
}
|
||||
|
@ -1,346 +0,0 @@
|
||||
/*
|
||||
* 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 org.futo.inputmethod.accessibility;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityEventCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityRecordCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
|
||||
import org.futo.inputmethod.keyboard.Key;
|
||||
import org.futo.inputmethod.keyboard.Keyboard;
|
||||
import org.futo.inputmethod.keyboard.KeyboardView;
|
||||
import org.futo.inputmethod.latin.common.CoordinateUtils;
|
||||
import org.futo.inputmethod.latin.settings.Settings;
|
||||
import org.futo.inputmethod.latin.settings.SettingsValues;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Exposes a virtual view sub-tree for {@link KeyboardView} and generates
|
||||
* {@link AccessibilityEvent}s for individual {@link Key}s.
|
||||
* <p>
|
||||
* A virtual sub-tree is composed of imaginary {@link View}s that are reported
|
||||
* as a part of the view hierarchy for accessibility purposes. This enables
|
||||
* custom views that draw complex content to report them selves as a tree of
|
||||
* virtual views, thus conveying their logical structure.
|
||||
* </p>
|
||||
*/
|
||||
final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView>
|
||||
extends AccessibilityNodeProviderCompat {
|
||||
private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName();
|
||||
|
||||
// From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}.
|
||||
private static final int UNDEFINED = Integer.MAX_VALUE;
|
||||
|
||||
private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
|
||||
private final AccessibilityUtils mAccessibilityUtils;
|
||||
|
||||
/** Temporary rect used to calculate in-screen bounds. */
|
||||
private final Rect mTempBoundsInScreen = new Rect();
|
||||
|
||||
/** The parent view's cached on-screen location. */
|
||||
private final int[] mParentLocation = CoordinateUtils.newInstance();
|
||||
|
||||
/** The virtual view identifier for the focused node. */
|
||||
private int mAccessibilityFocusedView = UNDEFINED;
|
||||
|
||||
/** The virtual view identifier for the hovering node. */
|
||||
private int mHoveringNodeId = UNDEFINED;
|
||||
|
||||
/** The keyboard view to provide an accessibility node info. */
|
||||
private final KV mKeyboardView;
|
||||
/** The accessibility delegate. */
|
||||
private final KeyboardAccessibilityDelegate<KV> mDelegate;
|
||||
|
||||
/** The current keyboard. */
|
||||
private Keyboard mKeyboard;
|
||||
|
||||
public KeyboardAccessibilityNodeProvider(final KV keyboardView,
|
||||
final KeyboardAccessibilityDelegate<KV> delegate) {
|
||||
super();
|
||||
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
|
||||
mAccessibilityUtils = AccessibilityUtils.getInstance();
|
||||
mKeyboardView = keyboardView;
|
||||
mDelegate = delegate;
|
||||
|
||||
// Since this class is constructed lazily, we might not get a subsequent
|
||||
// call to setKeyboard() and therefore need to call it now.
|
||||
setKeyboard(keyboardView.getKeyboard());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the keyboard represented by this node provider.
|
||||
*
|
||||
* @param keyboard The keyboard that is being set to the keyboard view.
|
||||
*/
|
||||
public void setKeyboard(final Keyboard keyboard) {
|
||||
mKeyboard = keyboard;
|
||||
}
|
||||
|
||||
private Key getKeyOf(final int virtualViewId) {
|
||||
if (mKeyboard == null) {
|
||||
return null;
|
||||
}
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
// Use a virtual view id as an index of the sorted keys list.
|
||||
if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) {
|
||||
return sortedKeys.get(virtualViewId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getVirtualViewIdOf(final Key key) {
|
||||
if (mKeyboard == null) {
|
||||
return View.NO_ID;
|
||||
}
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
final int size = sortedKeys.size();
|
||||
for (int index = 0; index < size; index++) {
|
||||
if (sortedKeys.get(index) == key) {
|
||||
// Use an index of the sorted keys list as a virtual view id.
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return View.NO_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and populates an {@link AccessibilityEvent} for the specified key
|
||||
* and event type.
|
||||
*
|
||||
* @param key A key on the host keyboard view.
|
||||
* @param eventType The event type to create.
|
||||
* @return A populated {@link AccessibilityEvent} for the key.
|
||||
* @see AccessibilityEvent
|
||||
*/
|
||||
public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
|
||||
final int virtualViewId = getVirtualViewIdOf(key);
|
||||
final String keyDescription = getKeyDescription(key);
|
||||
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
|
||||
event.setPackageName(mKeyboardView.getContext().getPackageName());
|
||||
event.setClassName(key.getClass().getName());
|
||||
event.setContentDescription(keyDescription);
|
||||
event.setEnabled(true);
|
||||
final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
|
||||
record.setSource(mKeyboardView, virtualViewId);
|
||||
return event;
|
||||
}
|
||||
|
||||
public void onHoverEnterTo(final Key key) {
|
||||
final int id = getVirtualViewIdOf(key);
|
||||
if (id == View.NO_ID) {
|
||||
return;
|
||||
}
|
||||
// Start hovering on the key. Because our accessibility model is lift-to-type, we should
|
||||
// report the node info without click and long click actions to avoid unnecessary
|
||||
// announcements.
|
||||
mHoveringNodeId = id;
|
||||
// Invalidate the node info of the key.
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
|
||||
}
|
||||
|
||||
public void onHoverExitFrom(final Key key) {
|
||||
mHoveringNodeId = UNDEFINED;
|
||||
// Invalidate the node info of the key to be able to revert the change we have done
|
||||
// in {@link #onHoverEnterTo(Key)}.
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
|
||||
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
|
||||
* view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
|
||||
* the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
|
||||
* <p>
|
||||
* A virtual descendant is an imaginary View that is reported as a part of
|
||||
* the view hierarchy for accessibility purposes. This enables custom views
|
||||
* that draw complex content to report them selves as a tree of virtual
|
||||
* views, thus conveying their logical structure.
|
||||
* </p>
|
||||
* <p>
|
||||
* The implementer is responsible for obtaining an accessibility node info
|
||||
* from the pool of reusable instances and setting the desired properties of
|
||||
* the node info before returning it.
|
||||
* </p>
|
||||
*
|
||||
* @param virtualViewId A client defined virtual view id.
|
||||
* @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host
|
||||
* View.
|
||||
* @see AccessibilityNodeInfoCompat
|
||||
*/
|
||||
@Override
|
||||
public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) {
|
||||
if (virtualViewId == UNDEFINED) {
|
||||
return null;
|
||||
}
|
||||
if (virtualViewId == View.NO_ID) {
|
||||
// We are requested to create an AccessibilityNodeInfo describing
|
||||
// this View, i.e. the root of the virtual sub-tree.
|
||||
final AccessibilityNodeInfoCompat rootInfo =
|
||||
AccessibilityNodeInfoCompat.obtain(mKeyboardView);
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
|
||||
updateParentLocation();
|
||||
|
||||
// Add the virtual children of the root View.
|
||||
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||
final int size = sortedKeys.size();
|
||||
for (int index = 0; index < size; index++) {
|
||||
final Key key = sortedKeys.get(index);
|
||||
if (key.isSpacer()) {
|
||||
continue;
|
||||
}
|
||||
// Use an index of the sorted keys list as a virtual view id.
|
||||
rootInfo.addChild(mKeyboardView, index);
|
||||
}
|
||||
return rootInfo;
|
||||
}
|
||||
|
||||
// Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
|
||||
final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
|
||||
populateNodeForVirtualView(virtualViewId, info);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public void populateNodeForVirtualView(int virtualViewId, @NonNull AccessibilityNodeInfoCompat info) {
|
||||
// Find the key that corresponds to the given virtual view id.
|
||||
final Key key = getKeyOf(virtualViewId);
|
||||
if (key == null) {
|
||||
Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
|
||||
return;
|
||||
}
|
||||
final String keyDescription = getKeyDescription(key);
|
||||
final Rect boundsInParent = key.getHitBox();
|
||||
|
||||
// Calculate the key's in-screen bounds.
|
||||
mTempBoundsInScreen.set(boundsInParent);
|
||||
mTempBoundsInScreen.offset(
|
||||
CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
|
||||
final Rect boundsInScreen = mTempBoundsInScreen;
|
||||
|
||||
info.setPackageName(mKeyboardView.getContext().getPackageName());
|
||||
info.setClassName(key.getClass().getName());
|
||||
info.setContentDescription(keyDescription);
|
||||
info.setBoundsInParent(boundsInParent);
|
||||
info.setBoundsInScreen(boundsInScreen);
|
||||
info.setParent(mKeyboardView);
|
||||
info.setSource(mKeyboardView, virtualViewId);
|
||||
info.setEnabled(key.isEnabled());
|
||||
info.setVisibleToUser(true);
|
||||
info.setTextEntryKey(true);
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||
if (key.isLongPressEnabled()) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
|
||||
}
|
||||
|
||||
if (mAccessibilityFocusedView == virtualViewId) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
||||
} else {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAction(final int virtualViewId, final int action,
|
||||
final Bundle arguments) {
|
||||
final Key key = getKeyOf(virtualViewId);
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
return performActionForKey(key, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the specified accessibility action for the given key.
|
||||
*
|
||||
* @param key The on which to perform the action.
|
||||
* @param action The action to perform.
|
||||
* @return The result of performing the action, or false if the action is not supported.
|
||||
*/
|
||||
boolean performActionForKey(final Key key, final int action) {
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
|
||||
mAccessibilityFocusedView = getVirtualViewIdOf(key);
|
||||
sendAccessibilityEventForKey(
|
||||
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
return true;
|
||||
case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
|
||||
mAccessibilityFocusedView = UNDEFINED;
|
||||
sendAccessibilityEventForKey(
|
||||
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
|
||||
return true;
|
||||
case AccessibilityNodeInfoCompat.ACTION_CLICK:
|
||||
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||
mDelegate.performClickOn(key);
|
||||
return true;
|
||||
case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
|
||||
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
|
||||
mDelegate.performLongClickOn(key);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an accessibility event for the given {@link Key}.
|
||||
*
|
||||
* @param key The key that's sending the event.
|
||||
* @param eventType The type of event to send.
|
||||
*/
|
||||
void sendAccessibilityEventForKey(final Key key, final int eventType) {
|
||||
final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
|
||||
mAccessibilityUtils.requestSendAccessibilityEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the context-specific description for a {@link Key}.
|
||||
*
|
||||
* @param key The key to describe.
|
||||
* @return The context-specific description of the key.
|
||||
*/
|
||||
private String getKeyDescription(final Key key) {
|
||||
final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo;
|
||||
final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
|
||||
final SettingsValues currentSettings = Settings.getInstance().getCurrent();
|
||||
final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
|
||||
mKeyboardView.getContext(), mKeyboard, key, shouldObscure);
|
||||
if (currentSettings.isWordSeparator(key.getCode())) {
|
||||
return mAccessibilityUtils.getAutoCorrectionDescription(
|
||||
keyCodeDescription, shouldObscure);
|
||||
}
|
||||
return keyCodeDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the parent's on-screen location.
|
||||
*/
|
||||
private void updateParentLocation() {
|
||||
mKeyboardView.getLocationOnScreen(mParentLocation);
|
||||
}
|
||||
}
|
@ -222,34 +222,6 @@ public final class MainKeyboardAccessibilityDelegate
|
||||
super.performClickOn(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverEnterTo(final Key key) {
|
||||
final int x = key.getHitBox().centerX();
|
||||
final int y = key.getHitBox().centerY();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnterTo: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||
}
|
||||
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
|
||||
return;
|
||||
}
|
||||
// This hover enter event points to the key that isn't in the ignoring region.
|
||||
// Further hover events should be handled.
|
||||
mBoundsToIgnoreHoverEvent.setEmpty();
|
||||
super.onHoverEnterTo(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverExitFrom(final Key key) {
|
||||
final int x = key.getHitBox().centerX();
|
||||
final int y = key.getHitBox().centerY();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExitFrom: key=" + key
|
||||
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||
}
|
||||
super.onHoverExitFrom(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void performLongClickOn(final Key key) {
|
||||
if (DEBUG_HOVER) {
|
||||
|
@ -19,6 +19,10 @@ package org.futo.inputmethod.accessibility;
|
||||
import android.graphics.Rect;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
|
||||
import androidx.core.view.accessibility.AccessibilityEventCompat;
|
||||
import androidx.core.view.accessibility.AccessibilityRecordCompat;
|
||||
|
||||
import org.futo.inputmethod.keyboard.Key;
|
||||
import org.futo.inputmethod.keyboard.KeyDetector;
|
||||
@ -59,62 +63,21 @@ public class MoreKeysKeyboardAccessibilityDelegate
|
||||
sendWindowStateChanged(mCloseAnnounceResId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverEnter(final MotionEvent event) {
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event));
|
||||
}
|
||||
super.onHoverEnter(event);
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
final int pointerId = event.getPointerId(actionIndex);
|
||||
final long eventTime = event.getEventTime();
|
||||
mKeyboardView.onDownEvent(x, y, pointerId, eventTime);
|
||||
public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
|
||||
final int virtualViewId = getVirtualViewIdOf(key);
|
||||
final String keyDescription = getKeyDescription(key);
|
||||
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
|
||||
event.setPackageName(mKeyboardView.getContext().getPackageName());
|
||||
event.setClassName(key.getClass().getName());
|
||||
event.setContentDescription(keyDescription);
|
||||
event.setEnabled(true);
|
||||
final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
|
||||
record.setSource(mKeyboardView, virtualViewId);
|
||||
return event;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverMove(final MotionEvent event) {
|
||||
super.onHoverMove(event);
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
final int pointerId = event.getPointerId(actionIndex);
|
||||
final long eventTime = event.getEventTime();
|
||||
mKeyboardView.onMoveEvent(x, y, pointerId, eventTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHoverExit(final MotionEvent event) {
|
||||
final Key lastKey = getLastHoverKey();
|
||||
if (DEBUG_HOVER) {
|
||||
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
|
||||
}
|
||||
if (lastKey != null) {
|
||||
super.onHoverExitFrom(lastKey);
|
||||
}
|
||||
setLastHoverKey(null);
|
||||
final int actionIndex = event.getActionIndex();
|
||||
final int x = (int)event.getX(actionIndex);
|
||||
final int y = (int)event.getY(actionIndex);
|
||||
final int pointerId = event.getPointerId(actionIndex);
|
||||
final long eventTime = event.getEventTime();
|
||||
// A hover exit event at one pixel width or height area on the edges of more keys keyboard
|
||||
// are treated as closing.
|
||||
mMoreKeysKeyboardValidBounds.set(0, 0, mKeyboardView.getWidth(), mKeyboardView.getHeight());
|
||||
mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL);
|
||||
if (mMoreKeysKeyboardValidBounds.contains(x, y)) {
|
||||
// Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
|
||||
// exit event selects a key.
|
||||
mKeyboardView.onUpEvent(x, y, pointerId, eventTime);
|
||||
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||
// {@link PointerTracker}.
|
||||
PointerTracker.dismissAllMoreKeysPanels();
|
||||
return;
|
||||
}
|
||||
// Close the more keys keyboard.
|
||||
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||
// {@link PointerTracker}.
|
||||
PointerTracker.dismissAllMoreKeysPanels();
|
||||
public void onKeyHovered(Key k) {
|
||||
AccessibilityEvent event = createAccessibilityEvent(k, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||
AccessibilityUtils.getInstance().requestSendAccessibilityEvent(event);
|
||||
}
|
||||
}
|
||||
|
@ -26,10 +26,12 @@ import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@ -752,17 +754,25 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean onHoverEvent(final MotionEvent event) {
|
||||
final MainKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
|
||||
if (accessibilityDelegate != null
|
||||
&& AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
|
||||
return accessibilityDelegate.onHoverEvent(event);
|
||||
public boolean dispatchHoverEvent(MotionEvent event) {
|
||||
return (mAccessibilityDelegate != null && AccessibilityUtils.getInstance().isTouchExplorationEnabled() && mAccessibilityDelegate.dispatchHoverEvent(event))
|
||||
|| super.dispatchHoverEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
return (mAccessibilityDelegate != null && AccessibilityUtils.getInstance().isTouchExplorationEnabled() && mAccessibilityDelegate.dispatchKeyEvent(event))
|
||||
|| super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusChanged(boolean gainFocus, int direction,
|
||||
Rect previouslyFocusedRect) {
|
||||
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
if(mAccessibilityDelegate != null && AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
|
||||
mAccessibilityDelegate.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
}
|
||||
return super.onHoverEvent(event);
|
||||
}
|
||||
|
||||
public void updateShortcutKey(final boolean available) {
|
||||
|
@ -20,8 +20,10 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -211,6 +213,10 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
|
||||
final Key newKey = mKeyDetector.detectHitKey(x, y);
|
||||
if (newKey == oldKey) {
|
||||
return newKey;
|
||||
} else {
|
||||
if(mAccessibilityDelegate != null && AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
|
||||
mAccessibilityDelegate.onKeyHovered(newKey);
|
||||
}
|
||||
}
|
||||
// A new key is detected.
|
||||
if (oldKey != null) {
|
||||
@ -281,17 +287,25 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean onHoverEvent(final MotionEvent event) {
|
||||
final MoreKeysKeyboardAccessibilityDelegate accessibilityDelegate = mAccessibilityDelegate;
|
||||
if (accessibilityDelegate != null
|
||||
&& AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
|
||||
return accessibilityDelegate.onHoverEvent(event);
|
||||
public boolean dispatchHoverEvent(MotionEvent event) {
|
||||
return (mAccessibilityDelegate != null && AccessibilityUtils.getInstance().isTouchExplorationEnabled() && mAccessibilityDelegate.dispatchHoverEvent(event))
|
||||
|| super.dispatchHoverEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
return (mAccessibilityDelegate != null && AccessibilityUtils.getInstance().isTouchExplorationEnabled() && mAccessibilityDelegate.dispatchKeyEvent(event))
|
||||
|| super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFocusChanged(boolean gainFocus, int direction,
|
||||
Rect previouslyFocusedRect) {
|
||||
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
if(mAccessibilityDelegate != null && AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
|
||||
mAccessibilityDelegate.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
}
|
||||
return super.onHoverEvent(event);
|
||||
}
|
||||
|
||||
private View getContainerView() {
|
||||
|
Loading…
Reference in New Issue
Block a user