diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index fdbec206ce9f5..231f9132e6aeb 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -710,18 +710,17 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable for (int i = 0; i < spanCount; i++) { int spanStart = starts[i]; - int spanEnd = ends[i]; - if (spanStart > gapstart) { spanStart -= gaplen; } - if (spanEnd > gapstart) { - spanEnd -= gaplen; - } - if (spanStart > queryEnd) { continue; } + + int spanEnd = ends[i]; + if (spanEnd > gapstart) { + spanEnd -= gaplen; + } if (spanEnd < queryStart) { continue; } diff --git a/core/java/android/text/style/SpellCheckSpan.java b/core/java/android/text/style/SpellCheckSpan.java index caaae99630a14..0d8a1034ed8f6 100644 --- a/core/java/android/text/style/SpellCheckSpan.java +++ b/core/java/android/text/style/SpellCheckSpan.java @@ -39,8 +39,8 @@ public class SpellCheckSpan implements ParcelableSpan { mSpellCheckInProgress = (src.readInt() != 0); } - public void setSpellCheckInProgress() { - mSpellCheckInProgress = true; + public void setSpellCheckInProgress(boolean inProgress) { + mSpellCheckInProgress = inProgress; } public boolean isSpellCheckInProgress() { diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java index 6b2f3e4faa19b..5d8db2f37122a 100644 --- a/core/java/android/widget/SpellChecker.java +++ b/core/java/android/widget/SpellChecker.java @@ -22,7 +22,6 @@ import android.text.Selection; import android.text.Spanned; import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; -import android.util.Log; import android.view.textservice.SpellCheckerSession; import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; import android.view.textservice.SuggestionsInfo; @@ -40,23 +39,21 @@ import java.util.Locale; * @hide */ public class SpellChecker implements SpellCheckerSessionListener { - private static final String LOG_TAG = "SpellChecker"; - private static final boolean DEBUG_SPELL_CHECK = false; - private static final int DELAY_BEFORE_SPELL_CHECK = 400; // milliseconds private final TextView mTextView; final SpellCheckerSession mSpellCheckerSession; final int mCookie; - // Paired arrays for the (id, spellCheckSpan) pair. mIndex is the next available position + // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated + // SpellCheckSpan has been recycled and can be-reused. + // May contain null SpellCheckSpans after a given index. private int[] mIds; private SpellCheckSpan[] mSpellCheckSpans; - // The actual current number of used slots in the above arrays + // The mLength first elements of the above arrays have been initialized private int mLength; private int mSpanSequenceCounter = 0; - private Runnable mChecker; public SpellChecker(TextView textView) { mTextView = textView; @@ -69,7 +66,7 @@ public class SpellChecker implements SpellCheckerSessionListener { mCookie = hashCode(); // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand - final int size = ArrayUtils.idealObjectArraySize(4); + final int size = ArrayUtils.idealObjectArraySize(1); mIds = new int[size]; mSpellCheckSpans = new SpellCheckSpan[size]; mLength = 0; @@ -89,73 +86,50 @@ public class SpellChecker implements SpellCheckerSessionListener { } } - public void addSpellCheckSpan(SpellCheckSpan spellCheckSpan) { - int length = mIds.length; - if (mLength >= length) { - final int newSize = length * 2; + private int nextSpellCheckSpanIndex() { + for (int i = 0; i < mLength; i++) { + if (mIds[i] < 0) return i; + } + + if (mLength == mSpellCheckSpans.length) { + final int newSize = mLength * 2; int[] newIds = new int[newSize]; SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; - System.arraycopy(mIds, 0, newIds, 0, length); - System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, length); + System.arraycopy(mIds, 0, newIds, 0, mLength); + System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength); mIds = newIds; mSpellCheckSpans = newSpellCheckSpans; } - mIds[mLength] = mSpanSequenceCounter++; - mSpellCheckSpans[mLength] = spellCheckSpan; + mSpellCheckSpans[mLength] = new SpellCheckSpan(); mLength++; + return mLength - 1; + } - if (DEBUG_SPELL_CHECK) { - final Editable mText = (Editable) mTextView.getText(); - int start = mText.getSpanStart(spellCheckSpan); - int end = mText.getSpanEnd(spellCheckSpan); - if (start >= 0 && end >= 0) { - Log.d(LOG_TAG, "Schedule check " + mText.subSequence(start, end)); - } else { - Log.d(LOG_TAG, "Schedule check EMPTY!"); - } - } - - scheduleSpellCheck(); + public void addSpellCheckSpan(int wordStart, int wordEnd) { + final int index = nextSpellCheckSpanIndex(); + ((Editable) mTextView.getText()).setSpan(mSpellCheckSpans[index], wordStart, wordEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mIds[index] = mSpanSequenceCounter++; } public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) { for (int i = 0; i < mLength; i++) { if (mSpellCheckSpans[i] == spellCheckSpan) { - removeAtIndex(i); + mSpellCheckSpans[i].setSpellCheckInProgress(false); + mIds[i] = -1; return; } } } - private void removeAtIndex(int i) { - System.arraycopy(mIds, i + 1, mIds, i, mLength - i - 1); - System.arraycopy(mSpellCheckSpans, i + 1, mSpellCheckSpans, i, mLength - i - 1); - mLength--; - } - public void onSelectionChanged() { - scheduleSpellCheck(); + spellCheck(); } - private void scheduleSpellCheck() { - if (mLength == 0) return; + public void spellCheck() { if (mSpellCheckerSession == null) return; - if (mChecker != null) { - mTextView.removeCallbacks(mChecker); - } - if (mChecker == null) { - mChecker = new Runnable() { - public void run() { - spellCheck(); - } - }; - } - mTextView.postDelayed(mChecker, DELAY_BEFORE_SPELL_CHECK); - } - - private void spellCheck() { final Editable editable = (Editable) mTextView.getText(); final int selectionStart = Selection.getSelectionStart(editable); final int selectionEnd = Selection.getSelectionEnd(editable); @@ -164,8 +138,7 @@ public class SpellChecker implements SpellCheckerSessionListener { int textInfosCount = 0; for (int i = 0; i < mLength; i++) { - SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; - + final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; if (spellCheckSpan.isSpellCheckInProgress()) continue; final int start = editable.getSpanStart(spellCheckSpan); @@ -174,7 +147,7 @@ public class SpellChecker implements SpellCheckerSessionListener { // Do not check this word if the user is currently editing it if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) { final String word = editable.subSequence(start, end).toString(); - spellCheckSpan.setSpellCheckInProgress(); + spellCheckSpan.setSpellCheckInProgress(true); textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); } } @@ -196,27 +169,18 @@ public class SpellChecker implements SpellCheckerSessionListener { for (int i = 0; i < results.length; i++) { SuggestionsInfo suggestionsInfo = results[i]; if (suggestionsInfo.getCookie() != mCookie) continue; - final int sequenceNumber = suggestionsInfo.getSequence(); - // Starting from the end, to limit the number of array copy while removing - for (int j = mLength - 1; j >= 0; j--) { + + for (int j = 0; j < mLength; j++) { + final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; + if (sequenceNumber == mIds[j]) { - SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; final int attributes = suggestionsInfo.getSuggestionsAttributes(); boolean isInDictionary = ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); boolean looksLikeTypo = ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); - if (DEBUG_SPELL_CHECK) { - final int start = editable.getSpanStart(spellCheckSpan); - final int end = editable.getSpanEnd(spellCheckSpan); - Log.d(LOG_TAG, "Result sequence=" + suggestionsInfo.getSequence() + " " + - editable.subSequence(start, end) + - "\t" + (isInDictionary?"IN_DICT" : "NOT_DICT") + - "\t" + (looksLikeTypo?"TYPO" : "NOT_TYPO")); - } - if (!isInDictionary && looksLikeTypo) { String[] suggestions = getSuggestions(suggestionsInfo); if (suggestions.length > 0) { @@ -230,13 +194,6 @@ public class SpellChecker implements SpellCheckerSessionListener { Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // TODO limit to the word rectangle region mTextView.invalidate(); - - if (DEBUG_SPELL_CHECK) { - String suggestionsString = ""; - for (String s : suggestions) { suggestionsString += s + "|"; } - Log.d(LOG_TAG, " Suggestions for " + sequenceNumber + " " + - editable.subSequence(start, end)+ " " + suggestionsString); - } } } editable.removeSpan(spellCheckSpan); @@ -246,9 +203,10 @@ public class SpellChecker implements SpellCheckerSessionListener { } private static String[] getSuggestions(SuggestionsInfo suggestionsInfo) { + // A negative suggestion count is possible final int len = Math.max(0, suggestionsInfo.getSuggestionsCount()); String[] suggestions = new String[len]; - for (int j = 0; j < len; ++j) { + for (int j = 0; j < len; j++) { suggestions[j] = suggestionsInfo.getSuggestionAt(j); } return suggestions; diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index bffdadcf420d2..51236e3a35da0 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -5537,7 +5537,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public boolean onCheckIsTextEditor() { return mInputType != EditorInfo.TYPE_NULL; } - + @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { if (onCheckIsTextEditor() && isEnabled()) { if (mInputMethodState == null) { @@ -7492,9 +7492,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ protected void onSelectionChanged(int selStart, int selEnd) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); - if (mSpellChecker != null) { - mSpellChecker.onSelectionChanged(); - } } /** @@ -7553,6 +7550,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener for (int i = 0; i < length; i++) { final int s = text.getSpanStart(spans[i]); final int e = text.getSpanEnd(spans[i]); + // Spans that are adjacent to the edited region will be handled in + // updateSpellCheckSpans. Result depends on what will be added (space or text) if (e == start || s == end) break; text.removeSpan(spans[i]); } @@ -7735,12 +7734,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (what instanceof SpellCheckSpan) { - if (newStart < 0) { - getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what); - } else if (oldStart < 0) { - getSpellChecker().addSpellCheckSpan((SpellCheckSpan) what); - } + if (newStart < 0 && what instanceof SpellCheckSpan) { + getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what); } } @@ -7750,8 +7745,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void updateSpellCheckSpans(int start, int end) { if (!isTextEditable() || !isSuggestionsEnabled() || !getSpellChecker().isSessionActive()) return; - Editable text = (Editable) mText; + Editable text = (Editable) mText; WordIterator wordIterator = getWordIterator(); wordIterator.setCharSequence(text); @@ -7770,57 +7765,75 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return; } + // We need to expand by one character because we want to include the spans that end/start + // at position start/end respectively. + SpellCheckSpan[] spellCheckSpans = text.getSpans(start - 1, end + 1, SpellCheckSpan.class); + SuggestionSpan[] suggestionSpans = text.getSpans(start - 1, end + 1, SuggestionSpan.class); + final int numberOfSpellCheckSpans = spellCheckSpans.length; + // Iterate over the newly added text and schedule new SpellCheckSpans while (wordStart <= end) { if (wordEnd >= start) { - // A word across the interval boundaries must remove boundary edition spans + // A new word has been created across the interval boundaries. Remove previous spans if (wordStart < start && wordEnd > start) { - removeEditionSpansAt(start, text); + removeSpansAt(start, spellCheckSpans, text); + removeSpansAt(start, suggestionSpans, text); } if (wordStart < end && wordEnd > end) { - removeEditionSpansAt(end, text); + removeSpansAt(end, spellCheckSpans, text); + removeSpansAt(end, suggestionSpans, text); } // Do not create new boundary spans if they already exist boolean createSpellCheckSpan = true; if (wordEnd == start) { - SpellCheckSpan[] spellCheckSpans = text.getSpans(start, start, - SpellCheckSpan.class); - if (spellCheckSpans.length > 0) createSpellCheckSpan = false; + for (int i = 0; i < numberOfSpellCheckSpans; i++) { + final int spanEnd = text.getSpanEnd(spellCheckSpans[i]); + if (spanEnd == start) { + createSpellCheckSpan = false; + break; + } + } } if (wordStart == end) { - SpellCheckSpan[] spellCheckSpans = text.getSpans(end, end, - SpellCheckSpan.class); - if (spellCheckSpans.length > 0) createSpellCheckSpan = false; + for (int i = 0; i < numberOfSpellCheckSpans; i++) { + final int spanStart = text.getSpanEnd(spellCheckSpans[i]); + if (spanStart == end) { + createSpellCheckSpan = false; + break; + } + } } if (createSpellCheckSpan) { - text.setSpan(new SpellCheckSpan(), wordStart, wordEnd, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + mSpellChecker.addSpellCheckSpan(wordStart, wordEnd); } } // iterate word by word wordEnd = wordIterator.following(wordEnd); - if (wordEnd == BreakIterator.DONE) return; + if (wordEnd == BreakIterator.DONE) break; wordStart = wordIterator.getBeginning(wordEnd); if (wordStart == BreakIterator.DONE) { Log.e(LOG_TAG, "Unable to find word beginning from " + wordEnd + "in " + mText); - return; + break; } } + + mSpellChecker.spellCheck(); } - private static void removeEditionSpansAt(int offset, Editable text) { - SuggestionSpan[] suggestionSpans = text.getSpans(offset, offset, SuggestionSpan.class); - for (int i = 0; i < suggestionSpans.length; i++) { - text.removeSpan(suggestionSpans[i]); - } - SpellCheckSpan[] spellCheckSpans = text.getSpans(offset, offset, SpellCheckSpan.class); - for (int i = 0; i < spellCheckSpans.length; i++) { - text.removeSpan(spellCheckSpans[i]); + private static void removeSpansAt(int offset, T[] spans, Editable text) { + final int length = spans.length; + for (int i = 0; i < length; i++) { + final T span = spans[i]; + final int start = text.getSpanStart(span); + if (start > offset) continue; + final int end = text.getSpanEnd(span); + if (end < offset) continue; + text.removeSpan(span); } } @@ -8381,6 +8394,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect(); hideControllers(); if (!selectAllGotFocus && mText.length() > 0) { + if (mSpellChecker != null) { + // When the cursor moves, the word that was typed may need spell check + mSpellChecker.onSelectionChanged(); + } if (isCursorInsideEasyCorrectionSpan()) { showSuggestions(); } else if (hasInsertionController()) {