Merge "Too many SpellCheckSpans are created."

This commit is contained in:
Gilles Debunne
2011-09-29 10:20:46 -07:00
committed by Android (Google) Code Review
4 changed files with 90 additions and 116 deletions

View File

@@ -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;
}

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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 <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 @@ 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()) {