diff options
author | Svetoslav Ganov <svetoslavganov@google.com> | 2012-04-27 19:30:38 -0700 |
---|---|---|
committer | Svetoslav Ganov <svetoslavganov@google.com> | 2012-05-07 17:31:52 -0700 |
commit | 6d17a936f73976971135aa1e6248662533343292 (patch) | |
tree | 947cfe7b940762dcfd74407e1e8f19c6142fec48 /core | |
parent | 2551e5a1d9990514d8116e352b8e5c2f10a9d303 (diff) | |
download | frameworks_base-6d17a936f73976971135aa1e6248662533343292.zip frameworks_base-6d17a936f73976971135aa1e6248662533343292.tar.gz frameworks_base-6d17a936f73976971135aa1e6248662533343292.tar.bz2 |
Text traversal at various granularities.
1. Implementing text content navigation at various granularities.
For views that have content description but no text the
content description is the traversed at character and word
granularities. For views that inherit from TextView the
supported granularities are character, word, line, and page.
bug:5932640
Conflicts:
core/java/android/view/View.java
Conflicts:
core/java/android/view/View.java
Change-Id: I66d1e16ce9ac5d6b49f036b17c087b2a7075e4c0
Diffstat (limited to 'core')
-rw-r--r-- | core/java/android/view/AccessibilityIterators.java | 352 | ||||
-rw-r--r-- | core/java/android/view/View.java | 168 | ||||
-rw-r--r-- | core/java/android/view/accessibility/AccessibilityEvent.java | 36 | ||||
-rw-r--r-- | core/java/android/view/accessibility/AccessibilityNodeInfo.java | 4 | ||||
-rw-r--r-- | core/java/android/widget/AccessibilityIterators.java | 219 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 93 |
6 files changed, 859 insertions, 13 deletions
diff --git a/core/java/android/view/AccessibilityIterators.java b/core/java/android/view/AccessibilityIterators.java new file mode 100644 index 0000000..386c866 --- /dev/null +++ b/core/java/android/view/AccessibilityIterators.java @@ -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); + } + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 5299d58..6260c54 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -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; @@ -1524,7 +1527,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 @@ -1590,6 +1594,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} * @@ -4714,11 +4723,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); } } @@ -5949,7 +5959,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)) { @@ -6050,6 +6061,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) { @@ -6447,9 +6462,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()) { @@ -6498,10 +6514,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 */ diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index f70ffa9..1a2a194 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -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; } @@ -791,6 +800,27 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } /** + * 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"); diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index c0696a9..fef24e2 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -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; diff --git a/core/java/android/widget/AccessibilityIterators.java b/core/java/android/widget/AccessibilityIterators.java new file mode 100644 index 0000000..e800e8d --- /dev/null +++ b/core/java/android/widget/AccessibilityIterators.java @@ -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); + } + } +} diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 8c81343..f2334ae 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -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; @@ -7712,6 +7712,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 @@ -7726,12 +7737,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(); @@ -8287,6 +8299,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}. */ |