Skip to content

Commit be5f49f

Browse files
author
Gilles Debunne
committed
Performance improvements for long text edition.
Limit each parse to batches of a few words, to keep the UI thread responsive. Possible optimizations for the future: - SpellCheck in a thread, but that requires some locking mecanism - Only spell check what is visible on screen. Will require additional spans to tag the pieces of text. This is a cherry pick of 145656 into ICS-MR1 Patch Set 2: Make the Runnable shared and stop it when detached. Change-Id: Ibf8e98274bda84b7176aac181ff267fc1f1fa4cb
1 parent df37228 commit be5f49f

File tree

1 file changed

+58
-14
lines changed

1 file changed

+58
-14
lines changed

core/java/android/widget/SpellChecker.java

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,18 @@
4343
*/
4444
public class SpellChecker implements SpellCheckerSessionListener {
4545

46-
private final static int MAX_SPELL_BATCH_SIZE = 50;
46+
// No more than this number of words will be parsed on each iteration to ensure a minimum
47+
// lock of the UI thread
48+
public static final int MAX_NUMBER_OF_WORDS = 50;
49+
50+
// Rough estimate, such that the word iterator interval usually does not need to be shifted
51+
public static final int AVERAGE_WORD_LENGTH = 7;
52+
53+
// When parsing, use a character window of that size. Will be shifted if needed
54+
public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;
55+
56+
// Pause between each spell check to keep the UI smooth
57+
private final static int SPELL_PAUSE_DURATION = 400; // milliseconds
4758

4859
private final TextView mTextView;
4960

@@ -71,6 +82,8 @@ public class SpellChecker implements SpellCheckerSessionListener {
7182

7283
private TextServicesManager mTextServicesManager;
7384

85+
private Runnable mSpellRunnable;
86+
7487
public SpellChecker(TextView textView) {
7588
mTextView = textView;
7689

@@ -141,6 +154,10 @@ public void closeSession() {
141154
for (int i = 0; i < length; i++) {
142155
mSpellParsers[i].finish();
143156
}
157+
158+
if (mSpellRunnable != null) {
159+
mTextView.removeCallbacks(mSpellRunnable);
160+
}
144161
}
145162

146163
private int nextSpellCheckSpanIndex() {
@@ -254,6 +271,7 @@ private void spellCheck() {
254271
System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
255272
textInfos = textInfosCopy;
256273
}
274+
257275
mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
258276
false /* TODO Set sequentialWords to true for initial spell check */);
259277
}
@@ -288,13 +306,29 @@ public void onGetSuggestions(SuggestionsInfo[] results) {
288306
}
289307
}
290308

291-
final int length = mSpellParsers.length;
292-
for (int i = 0; i < length; i++) {
293-
final SpellParser spellParser = mSpellParsers[i];
294-
if (!spellParser.isFinished()) {
295-
spellParser.parse();
296-
}
309+
scheduleNewSpellCheck();
310+
}
311+
312+
private void scheduleNewSpellCheck() {
313+
if (mSpellRunnable == null) {
314+
mSpellRunnable = new Runnable() {
315+
@Override
316+
public void run() {
317+
final int length = mSpellParsers.length;
318+
for (int i = 0; i < length; i++) {
319+
final SpellParser spellParser = mSpellParsers[i];
320+
if (!spellParser.isFinished()) {
321+
spellParser.parse();
322+
break; // run one spell parser at a time to bound running time
323+
}
324+
}
325+
}
326+
};
327+
} else {
328+
mTextView.removeCallbacks(mSpellRunnable);
297329
}
330+
331+
mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
298332
}
299333

300334
private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
@@ -383,7 +417,9 @@ public void parse() {
383417
// Iterate over the newly added text and schedule new SpellCheckSpans
384418
final int start = editable.getSpanStart(mRange);
385419
final int end = editable.getSpanEnd(mRange);
386-
mWordIterator.setCharSequence(editable, start, end);
420+
421+
int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
422+
mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
387423

388424
// Move back to the beginning of the current word, if any
389425
int wordStart = mWordIterator.preceding(start);
@@ -408,11 +444,16 @@ public void parse() {
408444
SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
409445
SuggestionSpan.class);
410446

411-
int nbWordsChecked = 0;
447+
int wordCount = 0;
412448
boolean scheduleOtherSpellCheck = false;
413449

414450
while (wordStart <= end) {
415451
if (wordEnd >= start && wordEnd > wordStart) {
452+
if (wordCount >= MAX_NUMBER_OF_WORDS) {
453+
scheduleOtherSpellCheck = true;
454+
break;
455+
}
456+
416457
// A new word has been created across the interval boundaries with this edit.
417458
// Previous spans (ended on start / started on end) removed, not valid anymore
418459
if (wordStart < start && wordEnd > start) {
@@ -448,17 +489,20 @@ public void parse() {
448489
}
449490

450491
if (createSpellCheckSpan) {
451-
if (nbWordsChecked == MAX_SPELL_BATCH_SIZE) {
452-
scheduleOtherSpellCheck = true;
453-
break;
454-
}
455492
addSpellCheckSpan(editable, wordStart, wordEnd);
456-
nbWordsChecked++;
457493
}
494+
wordCount++;
458495
}
459496

460497
// iterate word by word
498+
int originalWordEnd = wordEnd;
461499
wordEnd = mWordIterator.following(wordEnd);
500+
if ((wordIteratorWindowEnd < end) &&
501+
(wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
502+
wordIteratorWindowEnd = Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
503+
mWordIterator.setCharSequence(editable, originalWordEnd, wordIteratorWindowEnd);
504+
wordEnd = mWordIterator.following(originalWordEnd);
505+
}
462506
if (wordEnd == BreakIterator.DONE) break;
463507
wordStart = mWordIterator.getBeginning(wordEnd);
464508
if (wordStart == BreakIterator.DONE) {

0 commit comments

Comments
 (0)