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
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index fdbec20..231f913 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -710,18 +710,17 @@
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 caaae99..0d8a103 100644
--- a/core/java/android/text/style/SpellCheckSpan.java
+++ b/core/java/android/text/style/SpellCheckSpan.java
@@ -39,8 +39,8 @@
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 6b2f3e4..5d8db2f 100644
--- a/core/java/android/widget/SpellChecker.java
+++ b/core/java/android/widget/SpellChecker.java
@@ -22,7 +22,6 @@
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 @@
* @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 @@
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 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 @@
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 @@
// 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 @@
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 @@
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 @@
}
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 8ea55c6..8af22dc 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5537,7 +5537,7 @@
@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 @@
*/
protected void onSelectionChanged(int selStart, int selEnd) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
- if (mSpellChecker != null) {
- mSpellChecker.onSelectionChanged();
- }
}
/**
@@ -7553,6 +7550,8 @@
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 @@
}
}
- 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 @@
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 @@
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 <T> 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 @@
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()) {