From b062e81e3a16af43db3619d721aa522c137d1aa9 Mon Sep 17 00:00:00 2001 From: Gilles Debunne Date: Tue, 27 Sep 2011 14:58:37 -0700 Subject: [PATCH] Too many SpellCheckSpans are created. Removed the Runnable in SpellChecker, spell check is triggered at the end of updateSpellCheckSpans instead of when a new SpellCheckSpan is created. Cache the spans in updateSpellCheckSpans to limit the calls to getSpans. When typing, every new letter in a word will create a SpellCheckSpan (this is needed in case the user taps somewhere else on the screen) The SpellCheckSpans are pooled in SpellChecker to limit unnecessary new SpellCheckSpan creation. Minor optimization on test order in getSpans to avoid some calculation. Spell check is not started everytime the selection is changed (would be triggered when the insertion handle is moved). Explicitely do that only on tap. Change-Id: Ibacf80dd4ba098494e0b5ba0e58a362782fc8f71 --- .../android/text/SpannableStringBuilder.java | 11 +- .../android/text/style/SpellCheckSpan.java | 4 +- core/java/android/widget/SpellChecker.java | 110 ++++++------------ core/java/android/widget/TextView.java | 81 ++++++++----- 4 files changed, 90 insertions(+), 116 deletions(-) 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 8ea55c62530a0..8af22dc976e8a 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()) {