am 755b2146: am b2ee0d57: Merge "Text traversal at various granularities." into jb-dev

* commit '755b2146735c15deb0eb611430a7da1e363d82a1':
  Text traversal at various granularities.
This commit is contained in:
Svetoslav Ganov
2012-05-09 10:08:03 -07:00
committed by Android Git Automerger
7 changed files with 861 additions and 13 deletions

View 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);
}
}
}

View File

@@ -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
*/

View File

@@ -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");

View File

@@ -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;

View 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);
}
}
}

View File

@@ -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}.