From 6d17a936f73976971135aa1e6248662533343292 Mon Sep 17 00:00:00 2001 From: Svetoslav Ganov Date: Fri, 27 Apr 2012 19:30:38 -0700 Subject: [PATCH] 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 --- api/current.txt | 2 + .../android/view/AccessibilityIterators.java | 352 ++++++++++++++++++ core/java/android/view/View.java | 168 ++++++++- .../accessibility/AccessibilityEvent.java | 36 +- .../accessibility/AccessibilityNodeInfo.java | 4 +- .../widget/AccessibilityIterators.java | 219 +++++++++++ core/java/android/widget/TextView.java | 93 ++++- 7 files changed, 861 insertions(+), 13 deletions(-) create mode 100644 core/java/android/view/AccessibilityIterators.java create mode 100644 core/java/android/widget/AccessibilityIterators.java diff --git a/api/current.txt b/api/current.txt index 133a280427fd3..d19c08c0b117b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -25090,6 +25090,7 @@ package android.view.accessibility { method public void appendRecord(android.view.accessibility.AccessibilityRecord); method public int describeContents(); method public static java.lang.String eventTypeToString(int); + method public int getAction(); method public long getEventTime(); method public int getEventType(); method public int getMovementGranularity(); @@ -25100,6 +25101,7 @@ package android.view.accessibility { method public static android.view.accessibility.AccessibilityEvent obtain(int); method public static android.view.accessibility.AccessibilityEvent obtain(android.view.accessibility.AccessibilityEvent); method public static android.view.accessibility.AccessibilityEvent obtain(); + method public void setAction(int); method public void setEventTime(long); method public void setEventType(int); method public void setMovementGranularity(int); diff --git a/core/java/android/view/AccessibilityIterators.java b/core/java/android/view/AccessibilityIterators.java new file mode 100644 index 0000000000000..386c866d68479 --- /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 5299d58567ab1..6260c5468cb3e 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -47,7 +47,6 @@ 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.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 @@ -1589,6 +1593,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 @@ void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 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 void findViewsWithText(ArrayList outViews, CharSequence searched, i 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 void clearAccessibilityFocus() { 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 void resetAccessibilityStateChanged() { * 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 boolean performAccessibilityAction(int action, Bundle args) { 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 f70ffa95493aa..1a2a194f8211d 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -236,12 +236,19 @@ *
  • {@link #getClassName()} - The class name of the source.
  • *
  • {@link #getPackageName()} - The package name of the source.
  • *
  • {@link #getEventTime()} - The event time.
  • - *
  • {@link #getText()} - The text of the current text at the movement granularity.
  • + *
  • {@link #getMovementGranularity()} - Sets the granularity at which a view's text + * was traversed.
  • + *
  • {@link #getText()} - The text of the source's sub-tree.
  • + *
  • {@link #getFromIndex()} - The start of the next/previous text at the specified granularity + * - inclusive.
  • + *
  • {@link #getToIndex()} - The end of the next/previous text at the specified granularity + * - exclusive.
  • *
  • {@link #isPassword()} - Whether the source is password.
  • *
  • {@link #isEnabled()} - Whether the source is enabled.
  • *
  • {@link #getContentDescription()} - The content description of the source.
  • *
  • {@link #getMovementGranularity()} - Sets the granularity at which a view's text * was traversed.
  • + *
  • {@link #getAction()} - Gets traversal action which specifies the direction.
  • * *

    *

    @@ -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 mRecords = new ArrayList(); @@ -653,6 +661,7 @@ void init(AccessibilityEvent event) { super.init(event); mEventType = event.mEventType; mMovementGranularity = event.mMovementGranularity; + mAction = event.mAction; mEventTime = event.mEventTime; mPackageName = event.mPackageName; } @@ -790,6 +799,27 @@ public int getMovementGranularity() { 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 @@ protected void clear() { super.clear(); mEventType = 0; mMovementGranularity = 0; + mAction = 0; mPackageName = null; mEventTime = 0; while (!mRecords.isEmpty()) { @@ -896,6 +927,7 @@ public void initFromParcel(Parcel parcel) { 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 void writeToParcel(Parcel parcel, int flags) { 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 String toString() { 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 c0696a91115d3..fef24e2650621 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 0000000000000..e800e8df575f5 --- /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 8c81343e11ca9..f2334aea01065 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -26,7 +26,6 @@ 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.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 void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 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 void sendAccessibilityEvent(int eventType) { } /** - * 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(); @@ -8286,6 +8298,79 @@ private void createEditorIfNeeded(String reason) { } } + /** + * @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}.