diff options
| author | Svetoslav Ganov <svetoslavganov@google.com> | 2012-05-09 10:08:03 -0700 |
|---|---|---|
| committer | Android Git Automerger <android-git-automerger@android.com> | 2012-05-09 10:08:03 -0700 |
| commit | 4833ca2903e89eab93b353f00a1e4904a73d79bb (patch) | |
| tree | c0566eb93a2847ec051f9518ac93a72c19fed426 /core/java/android | |
| parent | b2b36eaf8d33a61a5e320e3bef4234484ffe1f63 (diff) | |
| parent | 755b2146735c15deb0eb611430a7da1e363d82a1 (diff) | |
am 755b2146: am b2ee0d57: Merge "Text traversal at various granularities." into jb-dev
* commit '755b2146735c15deb0eb611430a7da1e363d82a1':
Text traversal at various granularities.
Diffstat (limited to 'core/java/android')
| -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 000000000000..386c866d6847 --- /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 c054d387d402..5220f2d49ef2 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; @@ -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 @@ -1594,6 +1598,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 */ diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index f70ffa95493a..1a2a194f8211 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 c0696a91115d..fef24e265062 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 000000000000..e800e8df575f --- /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 ba814d325bd1..de3a9d358bca 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; @@ -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(); @@ -8276,6 +8288,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}. */ |
