* commit '755b2146735c15deb0eb611430a7da1e363d82a1': Text traversal at various granularities.
This commit is contained in:
352
core/java/android/view/AccessibilityIterators.java
Normal file
352
core/java/android/view/AccessibilityIterators.java
Normal file
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* 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 android.view;
|
||||
|
||||
import android.content.ComponentCallbacks;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import java.text.BreakIterator;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class contains the implementation of text segment iterators
|
||||
* for accessibility support.
|
||||
*
|
||||
* Note: Such iterators are needed in the view package since we want
|
||||
* to be able to iterator over content description of any view.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public final class AccessibilityIterators {
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public static interface TextSegmentIterator {
|
||||
public int[] following(int current);
|
||||
public int[] preceding(int current);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public static abstract class AbstractTextSegmentIterator implements TextSegmentIterator {
|
||||
protected static final int DONE = -1;
|
||||
|
||||
protected String mText;
|
||||
|
||||
private final int[] mSegment = new int[2];
|
||||
|
||||
public void initialize(String text) {
|
||||
mText = text;
|
||||
}
|
||||
|
||||
protected int[] getRange(int start, int end) {
|
||||
if (start < 0 || end < 0 || start == end) {
|
||||
return null;
|
||||
}
|
||||
mSegment[0] = start;
|
||||
mSegment[1] = end;
|
||||
return mSegment;
|
||||
}
|
||||
}
|
||||
|
||||
static class CharacterTextSegmentIterator extends AbstractTextSegmentIterator
|
||||
implements ComponentCallbacks {
|
||||
private static CharacterTextSegmentIterator sInstance;
|
||||
|
||||
private final Context mAppContext;
|
||||
|
||||
protected BreakIterator mImpl;
|
||||
|
||||
public static CharacterTextSegmentIterator getInstance(Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new CharacterTextSegmentIterator(context);
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private CharacterTextSegmentIterator(Context context) {
|
||||
mAppContext = context.getApplicationContext();
|
||||
Locale locale = mAppContext.getResources().getConfiguration().locale;
|
||||
onLocaleChanged(locale);
|
||||
ViewRootImpl.addConfigCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(String text) {
|
||||
super.initialize(text);
|
||||
mImpl.setText(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] following(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset >= textLegth) {
|
||||
return null;
|
||||
}
|
||||
int start = -1;
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
if (mImpl.isBoundary(offset)) {
|
||||
start = offset;
|
||||
}
|
||||
}
|
||||
if (start < 0) {
|
||||
start = mImpl.following(offset);
|
||||
}
|
||||
if (start < 0) {
|
||||
return null;
|
||||
}
|
||||
final int end = mImpl.following(start);
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] preceding(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset <= 0) {
|
||||
return null;
|
||||
}
|
||||
int end = -1;
|
||||
if (offset > mText.length()) {
|
||||
offset = mText.length();
|
||||
if (mImpl.isBoundary(offset)) {
|
||||
end = offset;
|
||||
}
|
||||
}
|
||||
if (end < 0) {
|
||||
end = mImpl.preceding(offset);
|
||||
}
|
||||
if (end < 0) {
|
||||
return null;
|
||||
}
|
||||
final int start = mImpl.preceding(end);
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
Configuration oldConfig = mAppContext.getResources().getConfiguration();
|
||||
final int changed = oldConfig.diff(newConfig);
|
||||
if ((changed & ActivityInfo.CONFIG_LOCALE) != 0) {
|
||||
Locale locale = newConfig.locale;
|
||||
onLocaleChanged(locale);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
protected void onLocaleChanged(Locale locale) {
|
||||
mImpl = BreakIterator.getCharacterInstance(locale);
|
||||
}
|
||||
}
|
||||
|
||||
static class WordTextSegmentIterator extends CharacterTextSegmentIterator {
|
||||
private static WordTextSegmentIterator sInstance;
|
||||
|
||||
public static WordTextSegmentIterator getInstance(Context context) {
|
||||
if (sInstance == null) {
|
||||
sInstance = new WordTextSegmentIterator(context);
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
private WordTextSegmentIterator(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLocaleChanged(Locale locale) {
|
||||
mImpl = BreakIterator.getWordInstance(locale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] following(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset >= mText.length()) {
|
||||
return null;
|
||||
}
|
||||
int start = -1;
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
if (mImpl.isBoundary(offset) && isLetterOrDigit(offset)) {
|
||||
start = offset;
|
||||
}
|
||||
}
|
||||
if (start < 0) {
|
||||
while ((offset = mImpl.following(offset)) != DONE) {
|
||||
if (isLetterOrDigit(offset)) {
|
||||
start = offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (start < 0) {
|
||||
return null;
|
||||
}
|
||||
final int end = mImpl.following(start);
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] preceding(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset <= 0) {
|
||||
return null;
|
||||
}
|
||||
int end = -1;
|
||||
if (offset > mText.length()) {
|
||||
offset = mText.length();
|
||||
if (mImpl.isBoundary(offset) && offset > 0 && isLetterOrDigit(offset - 1)) {
|
||||
end = offset;
|
||||
}
|
||||
}
|
||||
if (end < 0) {
|
||||
while ((offset = mImpl.preceding(offset)) != DONE) {
|
||||
if (offset > 0 && isLetterOrDigit(offset - 1)) {
|
||||
end = offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (end < 0) {
|
||||
return null;
|
||||
}
|
||||
final int start = mImpl.preceding(end);
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
private boolean isLetterOrDigit(int index) {
|
||||
if (index >= 0 && index < mText.length()) {
|
||||
final int codePoint = mText.codePointAt(index);
|
||||
return Character.isLetterOrDigit(codePoint);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class ParagraphTextSegmentIterator extends AbstractTextSegmentIterator {
|
||||
private static ParagraphTextSegmentIterator sInstance;
|
||||
|
||||
public static ParagraphTextSegmentIterator getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new ParagraphTextSegmentIterator();
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] following(int offset) {
|
||||
final int textLength = mText.length();
|
||||
if (textLength <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset >= textLength) {
|
||||
return null;
|
||||
}
|
||||
int start = -1;
|
||||
if (offset < 0) {
|
||||
start = 0;
|
||||
} else {
|
||||
for (int i = offset + 1; i < textLength; i++) {
|
||||
if (mText.charAt(i) == '\n') {
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (start < textLength && mText.charAt(start) == '\n') {
|
||||
start++;
|
||||
}
|
||||
if (start < 0) {
|
||||
return null;
|
||||
}
|
||||
int end = start;
|
||||
for (int i = end + 1; i < textLength; i++) {
|
||||
end = i;
|
||||
if (mText.charAt(i) == '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (end < textLength && mText.charAt(end) == '\n') {
|
||||
end++;
|
||||
}
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] preceding(int offset) {
|
||||
final int textLength = mText.length();
|
||||
if (textLength <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset <= 0) {
|
||||
return null;
|
||||
}
|
||||
int end = -1;
|
||||
if (offset > mText.length()) {
|
||||
end = mText.length();
|
||||
} else {
|
||||
if (offset > 0 && mText.charAt(offset - 1) == '\n') {
|
||||
offset--;
|
||||
}
|
||||
for (int i = offset - 1; i >= 0; i--) {
|
||||
if (i > 0 && mText.charAt(i - 1) == '\n') {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (end <= 0) {
|
||||
return null;
|
||||
}
|
||||
int start = end;
|
||||
while (start > 0 && mText.charAt(start - 1) == '\n') {
|
||||
start--;
|
||||
}
|
||||
if (start == 0 && mText.charAt(start) == '\n') {
|
||||
return null;
|
||||
}
|
||||
for (int i = start - 1; i >= 0; i--) {
|
||||
start = i;
|
||||
if (start > 0 && mText.charAt(i - 1) == '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
start = Math.max(0, start);
|
||||
return getRange(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ import android.os.Parcelable;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import android.os.SystemProperties;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.FloatProperty;
|
||||
import android.util.LocaleUtil;
|
||||
@@ -60,6 +59,10 @@ import android.util.Property;
|
||||
import android.util.SparseArray;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.AccessibilityIterators.TextSegmentIterator;
|
||||
import android.view.AccessibilityIterators.CharacterTextSegmentIterator;
|
||||
import android.view.AccessibilityIterators.WordTextSegmentIterator;
|
||||
import android.view.AccessibilityIterators.ParagraphTextSegmentIterator;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityEventSource;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
@@ -1528,7 +1531,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
|
||||
| AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
|
||||
| AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
|
||||
| AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
|
||||
| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED;
|
||||
| AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
|
||||
| AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;
|
||||
|
||||
/**
|
||||
* Temporary Rect currently for use in setBackground(). This will probably
|
||||
@@ -1593,6 +1597,11 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
|
||||
*/
|
||||
int mAccessibilityViewId = NO_ID;
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
private int mAccessibilityCursorPosition = -1;
|
||||
|
||||
/**
|
||||
* The view's tag.
|
||||
* {@hide}
|
||||
@@ -4699,11 +4708,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
|
||||
}
|
||||
|
||||
if (getContentDescription() != null) {
|
||||
if (mContentDescription != null && mContentDescription.length() > 0) {
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
|
||||
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
|
||||
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
|
||||
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
|
||||
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5929,7 +5939,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
|
||||
outViews.add(this);
|
||||
}
|
||||
} else if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0
|
||||
&& !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mContentDescription)) {
|
||||
&& (searched != null && searched.length() > 0)
|
||||
&& (mContentDescription != null && mContentDescription.length() > 0)) {
|
||||
String searchedLowerCase = searched.toString().toLowerCase();
|
||||
String contentDescriptionLowerCase = mContentDescription.toString().toLowerCase();
|
||||
if (contentDescriptionLowerCase.contains(searchedLowerCase)) {
|
||||
@@ -6030,6 +6041,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
|
||||
invalidate();
|
||||
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
|
||||
notifyAccessibilityStateChanged();
|
||||
|
||||
// Clear the text navigation state.
|
||||
setAccessibilityCursorPosition(-1);
|
||||
|
||||
// Try to move accessibility focus to the input focus.
|
||||
View rootView = getRootView();
|
||||
if (rootView != null) {
|
||||
@@ -6427,9 +6442,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
|
||||
* possible accessibility actions look at {@link AccessibilityNodeInfo}.
|
||||
*
|
||||
* @param action The action to perform.
|
||||
* @param arguments Optional action arguments.
|
||||
* @return Whether the action was performed.
|
||||
*/
|
||||
public boolean performAccessibilityAction(int action, Bundle args) {
|
||||
public boolean performAccessibilityAction(int action, Bundle arguments) {
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_CLICK: {
|
||||
if (isClickable()) {
|
||||
@@ -6478,10 +6494,150 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal
|
||||
return true;
|
||||
}
|
||||
} break;
|
||||
case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: {
|
||||
if (arguments != null) {
|
||||
final int granularity = arguments.getInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
|
||||
return nextAtGranularity(granularity);
|
||||
}
|
||||
} break;
|
||||
case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
|
||||
if (arguments != null) {
|
||||
final int granularity = arguments.getInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
|
||||
return previousAtGranularity(granularity);
|
||||
}
|
||||
} break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean nextAtGranularity(int granularity) {
|
||||
CharSequence text = getIterableTextForAccessibility();
|
||||
if (text != null && text.length() > 0) {
|
||||
return false;
|
||||
}
|
||||
TextSegmentIterator iterator = getIteratorForGranularity(granularity);
|
||||
if (iterator == null) {
|
||||
return false;
|
||||
}
|
||||
final int current = getAccessibilityCursorPosition();
|
||||
final int[] range = iterator.following(current);
|
||||
if (range == null) {
|
||||
setAccessibilityCursorPosition(-1);
|
||||
return false;
|
||||
}
|
||||
final int start = range[0];
|
||||
final int end = range[1];
|
||||
setAccessibilityCursorPosition(start);
|
||||
sendViewTextTraversedAtGranularityEvent(
|
||||
AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY,
|
||||
granularity, start, end);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean previousAtGranularity(int granularity) {
|
||||
CharSequence text = getIterableTextForAccessibility();
|
||||
if (text != null && text.length() > 0) {
|
||||
return false;
|
||||
}
|
||||
TextSegmentIterator iterator = getIteratorForGranularity(granularity);
|
||||
if (iterator == null) {
|
||||
return false;
|
||||
}
|
||||
final int selectionStart = getAccessibilityCursorPosition();
|
||||
final int current = selectionStart >= 0 ? selectionStart : text.length() + 1;
|
||||
final int[] range = iterator.preceding(current);
|
||||
if (range == null) {
|
||||
setAccessibilityCursorPosition(-1);
|
||||
return false;
|
||||
}
|
||||
final int start = range[0];
|
||||
final int end = range[1];
|
||||
setAccessibilityCursorPosition(end);
|
||||
sendViewTextTraversedAtGranularityEvent(
|
||||
AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
|
||||
granularity, start, end);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text reported for accessibility purposes.
|
||||
*
|
||||
* @return The accessibility text.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public CharSequence getIterableTextForAccessibility() {
|
||||
return mContentDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public int getAccessibilityCursorPosition() {
|
||||
return mAccessibilityCursorPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public void setAccessibilityCursorPosition(int position) {
|
||||
mAccessibilityCursorPosition = position;
|
||||
}
|
||||
|
||||
private void sendViewTextTraversedAtGranularityEvent(int action, int granularity,
|
||||
int fromIndex, int toIndex) {
|
||||
if (mParent == null) {
|
||||
return;
|
||||
}
|
||||
AccessibilityEvent event = AccessibilityEvent.obtain(
|
||||
AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
|
||||
onInitializeAccessibilityEvent(event);
|
||||
onPopulateAccessibilityEvent(event);
|
||||
event.setFromIndex(fromIndex);
|
||||
event.setToIndex(toIndex);
|
||||
event.setAction(action);
|
||||
event.setMovementGranularity(granularity);
|
||||
mParent.requestSendAccessibilityEvent(this, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
public TextSegmentIterator getIteratorForGranularity(int granularity) {
|
||||
switch (granularity) {
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: {
|
||||
CharSequence text = getIterableTextForAccessibility();
|
||||
if (text != null && text.length() > 0) {
|
||||
CharacterTextSegmentIterator iterator =
|
||||
CharacterTextSegmentIterator.getInstance(mContext);
|
||||
iterator.initialize(text.toString());
|
||||
return iterator;
|
||||
}
|
||||
} break;
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD: {
|
||||
CharSequence text = getIterableTextForAccessibility();
|
||||
if (text != null && text.length() > 0) {
|
||||
WordTextSegmentIterator iterator =
|
||||
WordTextSegmentIterator.getInstance(mContext);
|
||||
iterator.initialize(text.toString());
|
||||
return iterator;
|
||||
}
|
||||
} break;
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH: {
|
||||
CharSequence text = getIterableTextForAccessibility();
|
||||
if (text != null && text.length() > 0) {
|
||||
ParagraphTextSegmentIterator iterator =
|
||||
ParagraphTextSegmentIterator.getInstance();
|
||||
iterator.initialize(text.toString());
|
||||
return iterator;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
|
||||
@@ -236,12 +236,19 @@ import java.util.List;
|
||||
* <li>{@link #getClassName()} - The class name of the source.</li>
|
||||
* <li>{@link #getPackageName()} - The package name of the source.</li>
|
||||
* <li>{@link #getEventTime()} - The event time.</li>
|
||||
* <li>{@link #getText()} - The text of the current text at the movement granularity.</li>
|
||||
* <li>{@link #getMovementGranularity()} - Sets the granularity at which a view's text
|
||||
* was traversed.</li>
|
||||
* <li>{@link #getText()} - The text of the source's sub-tree.</li>
|
||||
* <li>{@link #getFromIndex()} - The start of the next/previous text at the specified granularity
|
||||
* - inclusive.</li>
|
||||
* <li>{@link #getToIndex()} - The end of the next/previous text at the specified granularity
|
||||
* - exclusive.</li>
|
||||
* <li>{@link #isPassword()} - Whether the source is password.</li>
|
||||
* <li>{@link #isEnabled()} - Whether the source is enabled.</li>
|
||||
* <li>{@link #getContentDescription()} - The content description of the source.</li>
|
||||
* <li>{@link #getMovementGranularity()} - Sets the granularity at which a view's text
|
||||
* was traversed.</li>
|
||||
* <li>{@link #getAction()} - Gets traversal action which specifies the direction.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
* <p>
|
||||
@@ -635,6 +642,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
|
||||
private CharSequence mPackageName;
|
||||
private long mEventTime;
|
||||
int mMovementGranularity;
|
||||
int mAction;
|
||||
|
||||
private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>();
|
||||
|
||||
@@ -653,6 +661,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
|
||||
super.init(event);
|
||||
mEventType = event.mEventType;
|
||||
mMovementGranularity = event.mMovementGranularity;
|
||||
mAction = event.mAction;
|
||||
mEventTime = event.mEventTime;
|
||||
mPackageName = event.mPackageName;
|
||||
}
|
||||
@@ -790,6 +799,27 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
|
||||
return mMovementGranularity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the performed action that triggered this event.
|
||||
*
|
||||
* @param action The action.
|
||||
*
|
||||
* @throws IllegalStateException If called from an AccessibilityService.
|
||||
*/
|
||||
public void setAction(int action) {
|
||||
enforceNotSealed();
|
||||
mAction = action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the performed action that triggered this event.
|
||||
*
|
||||
* @return The action.
|
||||
*/
|
||||
public int getAction() {
|
||||
return mAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cached instance if such is available or a new one is
|
||||
* instantiated with its type property set.
|
||||
@@ -879,6 +909,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
|
||||
super.clear();
|
||||
mEventType = 0;
|
||||
mMovementGranularity = 0;
|
||||
mAction = 0;
|
||||
mPackageName = null;
|
||||
mEventTime = 0;
|
||||
while (!mRecords.isEmpty()) {
|
||||
@@ -896,6 +927,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
|
||||
mSealed = (parcel.readInt() == 1);
|
||||
mEventType = parcel.readInt();
|
||||
mMovementGranularity = parcel.readInt();
|
||||
mAction = parcel.readInt();
|
||||
mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
|
||||
mEventTime = parcel.readLong();
|
||||
mConnectionId = parcel.readInt();
|
||||
@@ -947,6 +979,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
|
||||
parcel.writeInt(isSealed() ? 1 : 0);
|
||||
parcel.writeInt(mEventType);
|
||||
parcel.writeInt(mMovementGranularity);
|
||||
parcel.writeInt(mAction);
|
||||
TextUtils.writeToParcel(mPackageName, parcel, 0);
|
||||
parcel.writeLong(mEventTime);
|
||||
parcel.writeInt(mConnectionId);
|
||||
@@ -1004,6 +1037,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
|
||||
builder.append("; EventTime: ").append(mEventTime);
|
||||
builder.append("; PackageName: ").append(mPackageName);
|
||||
builder.append("; MovementGranularity: ").append(mMovementGranularity);
|
||||
builder.append("; Action: ").append(mAction);
|
||||
builder.append(super.toString());
|
||||
if (DEBUG) {
|
||||
builder.append("\n");
|
||||
|
||||
@@ -102,12 +102,12 @@ public class AccessibilityNodeInfo implements Parcelable {
|
||||
public static final int ACTION_CLEAR_SELECTION = 0x00000008;
|
||||
|
||||
/**
|
||||
* Action that long clicks on the node info.
|
||||
* Action that clicks on the node info.
|
||||
*/
|
||||
public static final int ACTION_CLICK = 0x00000010;
|
||||
|
||||
/**
|
||||
* Action that clicks on the node.
|
||||
* Action that long clicks on the node.
|
||||
*/
|
||||
public static final int ACTION_LONG_CLICK = 0x00000020;
|
||||
|
||||
|
||||
219
core/java/android/widget/AccessibilityIterators.java
Normal file
219
core/java/android/widget/AccessibilityIterators.java
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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 android.widget;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.view.AccessibilityIterators.AbstractTextSegmentIterator;
|
||||
|
||||
/**
|
||||
* This class contains the implementation of text segment iterators
|
||||
* for accessibility support.
|
||||
*/
|
||||
final class AccessibilityIterators {
|
||||
|
||||
static class LineTextSegmentIterator extends AbstractTextSegmentIterator {
|
||||
private static LineTextSegmentIterator sLineInstance;
|
||||
|
||||
protected static final int DIRECTION_START = -1;
|
||||
protected static final int DIRECTION_END = 1;
|
||||
|
||||
protected Layout mLayout;
|
||||
|
||||
public static LineTextSegmentIterator getInstance() {
|
||||
if (sLineInstance == null) {
|
||||
sLineInstance = new LineTextSegmentIterator();
|
||||
}
|
||||
return sLineInstance;
|
||||
}
|
||||
|
||||
public void initialize(Spannable text, Layout layout) {
|
||||
mText = text.toString();
|
||||
mLayout = layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] following(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset >= mText.length()) {
|
||||
return null;
|
||||
}
|
||||
int nextLine = -1;
|
||||
if (offset < 0) {
|
||||
nextLine = mLayout.getLineForOffset(0);
|
||||
} else {
|
||||
final int currentLine = mLayout.getLineForOffset(offset);
|
||||
if (currentLine < mLayout.getLineCount() - 1) {
|
||||
nextLine = currentLine + 1;
|
||||
}
|
||||
}
|
||||
if (nextLine < 0) {
|
||||
return null;
|
||||
}
|
||||
final int start = getLineEdgeIndex(nextLine, DIRECTION_START);
|
||||
final int end = getLineEdgeIndex(nextLine, DIRECTION_END) + 1;
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] preceding(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset <= 0) {
|
||||
return null;
|
||||
}
|
||||
int previousLine = -1;
|
||||
if (offset > mText.length()) {
|
||||
previousLine = mLayout.getLineForOffset(mText.length());
|
||||
} else {
|
||||
final int currentLine = mLayout.getLineForOffset(offset - 1);
|
||||
if (currentLine > 0) {
|
||||
previousLine = currentLine - 1;
|
||||
}
|
||||
}
|
||||
if (previousLine < 0) {
|
||||
return null;
|
||||
}
|
||||
final int start = getLineEdgeIndex(previousLine, DIRECTION_START);
|
||||
final int end = getLineEdgeIndex(previousLine, DIRECTION_END) + 1;
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
protected int getLineEdgeIndex(int lineNumber, int direction) {
|
||||
final int paragraphDirection = mLayout.getParagraphDirection(lineNumber);
|
||||
if (direction * paragraphDirection < 0) {
|
||||
return mLayout.getLineStart(lineNumber);
|
||||
} else {
|
||||
return mLayout.getLineEnd(lineNumber) - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class PageTextSegmentIterator extends LineTextSegmentIterator {
|
||||
private static PageTextSegmentIterator sPageInstance;
|
||||
|
||||
private TextView mView;
|
||||
|
||||
private final Rect mTempRect = new Rect();
|
||||
|
||||
public static PageTextSegmentIterator getInstance() {
|
||||
if (sPageInstance == null) {
|
||||
sPageInstance = new PageTextSegmentIterator();
|
||||
}
|
||||
return sPageInstance;
|
||||
}
|
||||
|
||||
public void initialize(TextView view) {
|
||||
super.initialize((Spannable) view.getIterableTextForAccessibility(), view.getLayout());
|
||||
mView = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] following(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset >= mText.length()) {
|
||||
return null;
|
||||
}
|
||||
if (!mView.getGlobalVisibleRect(mTempRect)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int currentLine = mLayout.getLineForOffset(offset);
|
||||
final int currentLineTop = mLayout.getLineTop(currentLine);
|
||||
final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
|
||||
- mView.getTotalPaddingBottom();
|
||||
|
||||
final int nextPageStartLine;
|
||||
final int nextPageEndLine;
|
||||
if (offset < 0) {
|
||||
nextPageStartLine = currentLine;
|
||||
final int nextPageEndY = currentLineTop + pageHeight;
|
||||
nextPageEndLine = mLayout.getLineForVertical(nextPageEndY);
|
||||
} else {
|
||||
final int nextPageStartY = currentLineTop + pageHeight;
|
||||
nextPageStartLine = mLayout.getLineForVertical(nextPageStartY) + 1;
|
||||
if (mLayout.getLineTop(nextPageStartLine) <= nextPageStartY) {
|
||||
return null;
|
||||
}
|
||||
final int nextPageEndY = nextPageStartY + pageHeight;
|
||||
nextPageEndLine = mLayout.getLineForVertical(nextPageEndY);
|
||||
}
|
||||
|
||||
final int start = getLineEdgeIndex(nextPageStartLine, DIRECTION_START);
|
||||
final int end = getLineEdgeIndex(nextPageEndLine, DIRECTION_END) + 1;
|
||||
|
||||
return getRange(start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] preceding(int offset) {
|
||||
final int textLegth = mText.length();
|
||||
if (textLegth <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (offset <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (!mView.getGlobalVisibleRect(mTempRect)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int currentLine = mLayout.getLineForOffset(offset);
|
||||
final int currentLineTop = mLayout.getLineTop(currentLine);
|
||||
final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
|
||||
- mView.getTotalPaddingBottom();
|
||||
|
||||
final int previousPageStartLine;
|
||||
final int previousPageEndLine;
|
||||
if (offset > mText.length()) {
|
||||
final int prevousPageStartY = mLayout.getHeight() - pageHeight;
|
||||
if (prevousPageStartY < 0) {
|
||||
return null;
|
||||
}
|
||||
previousPageStartLine = mLayout.getLineForVertical(prevousPageStartY);
|
||||
previousPageEndLine = mLayout.getLineCount() - 1;
|
||||
} else {
|
||||
final int prevousPageStartY;
|
||||
if (offset == mText.length()) {
|
||||
prevousPageStartY = mLayout.getHeight() - 2 * pageHeight;
|
||||
} else {
|
||||
prevousPageStartY = currentLineTop - 2 * pageHeight;
|
||||
}
|
||||
if (prevousPageStartY < 0) {
|
||||
return null;
|
||||
}
|
||||
previousPageStartLine = mLayout.getLineForVertical(prevousPageStartY);
|
||||
final int previousPageEndY = prevousPageStartY + pageHeight;
|
||||
previousPageEndLine = mLayout.getLineForVertical(previousPageEndY) - 1;
|
||||
}
|
||||
|
||||
final int start = getLineEdgeIndex(previousPageStartLine, DIRECTION_START);
|
||||
final int end = getLineEdgeIndex(previousPageEndLine, DIRECTION_END) + 1;
|
||||
|
||||
return getRange(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.XmlResourceParser;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
@@ -91,6 +90,7 @@ import android.util.AttributeSet;
|
||||
import android.util.FloatMath;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.AccessibilityIterators.TextSegmentIterator;
|
||||
import android.view.ActionMode;
|
||||
import android.view.DragEvent;
|
||||
import android.view.Gravity;
|
||||
@@ -7701,6 +7701,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
||||
if (!isPassword) {
|
||||
info.setText(getTextForAccessibility());
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(getContentDescription())
|
||||
&& !TextUtils.isEmpty(mText)) {
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
|
||||
info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
|
||||
info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
|
||||
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
|
||||
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
|
||||
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
|
||||
| AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -7715,12 +7726,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text reported for accessibility purposes. It is the
|
||||
* text if not empty or the hint.
|
||||
* Gets the text reported for accessibility purposes.
|
||||
*
|
||||
* @return The accessibility text.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
private CharSequence getTextForAccessibility() {
|
||||
public CharSequence getTextForAccessibility() {
|
||||
CharSequence text = getText();
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
text = getHint();
|
||||
@@ -8275,6 +8287,79 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public CharSequence getIterableTextForAccessibility() {
|
||||
if (getContentDescription() == null) {
|
||||
if (!(mText instanceof Spannable)) {
|
||||
setText(mText, BufferType.SPANNABLE);
|
||||
}
|
||||
return mText;
|
||||
}
|
||||
return super.getIterableTextForAccessibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public TextSegmentIterator getIteratorForGranularity(int granularity) {
|
||||
switch (granularity) {
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE: {
|
||||
Spannable text = (Spannable) getIterableTextForAccessibility();
|
||||
if (!TextUtils.isEmpty(text) && getLayout() != null) {
|
||||
AccessibilityIterators.LineTextSegmentIterator iterator =
|
||||
AccessibilityIterators.LineTextSegmentIterator.getInstance();
|
||||
iterator.initialize(text, getLayout());
|
||||
return iterator;
|
||||
}
|
||||
} break;
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE: {
|
||||
Spannable text = (Spannable) getIterableTextForAccessibility();
|
||||
if (!TextUtils.isEmpty(text) && getLayout() != null) {
|
||||
AccessibilityIterators.PageTextSegmentIterator iterator =
|
||||
AccessibilityIterators.PageTextSegmentIterator.getInstance();
|
||||
iterator.initialize(this);
|
||||
return iterator;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
return super.getIteratorForGranularity(granularity);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public int getAccessibilityCursorPosition() {
|
||||
if (TextUtils.isEmpty(getContentDescription())) {
|
||||
return getSelectionEnd();
|
||||
} else {
|
||||
return super.getAccessibilityCursorPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hide
|
||||
*/
|
||||
@Override
|
||||
public void setAccessibilityCursorPosition(int index) {
|
||||
if (getAccessibilityCursorPosition() == index) {
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(getContentDescription())) {
|
||||
if (index >= 0) {
|
||||
Selection.setSelection((Spannable) mText, index);
|
||||
} else {
|
||||
Selection.removeSelection((Spannable) mText);
|
||||
}
|
||||
} else {
|
||||
super.setAccessibilityCursorPosition(index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User interface state that is stored by TextView for implementing
|
||||
* {@link View#onSaveInstanceState}.
|
||||
|
||||
Reference in New Issue
Block a user