Merge "Bug 5250788: LatinIME slows down as amount of Text increases"
diff --git a/core/java/android/text/CharSequenceIterator.java b/core/java/android/text/CharSequenceIterator.java
deleted file mode 100644
index 4b8ac10..0000000
--- a/core/java/android/text/CharSequenceIterator.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.text;
-
-import java.text.CharacterIterator;
-
-/** {@hide} */
-public class CharSequenceIterator implements CharacterIterator {
- private final CharSequence mValue;
-
- private final int mLength;
- private int mIndex;
-
- public CharSequenceIterator(CharSequence value) {
- mValue = value;
- mLength = value.length();
- mIndex = 0;
- }
-
- @Override
- public Object clone() {
- try {
- return super.clone();
- } catch (CloneNotSupportedException e) {
- throw new AssertionError(e);
- }
- }
-
- /** {@inheritDoc} */
- public char current() {
- if (mIndex == mLength) {
- return DONE;
- }
- return mValue.charAt(mIndex);
- }
-
- /** {@inheritDoc} */
- public int getBeginIndex() {
- return 0;
- }
-
- /** {@inheritDoc} */
- public int getEndIndex() {
- return mLength;
- }
-
- /** {@inheritDoc} */
- public int getIndex() {
- return mIndex;
- }
-
- /** {@inheritDoc} */
- public char first() {
- return setIndex(0);
- }
-
- /** {@inheritDoc} */
- public char last() {
- return setIndex(mLength - 1);
- }
-
- /** {@inheritDoc} */
- public char next() {
- if (mIndex == mLength) {
- return DONE;
- }
- return setIndex(mIndex + 1);
- }
-
- /** {@inheritDoc} */
- public char previous() {
- if (mIndex == 0) {
- return DONE;
- }
- return setIndex(mIndex - 1);
- }
-
- /** {@inheritDoc} */
- public char setIndex(int index) {
- if ((index < 0) || (index > mLength)) {
- throw new IllegalArgumentException("Valid range is [" + 0 + "..." + mLength + "]");
- }
- mIndex = index;
- return current();
- }
-}
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
index b8728ee..e93039b 100644
--- a/core/java/android/text/method/ArrowKeyMovementMethod.java
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -35,11 +35,11 @@
(MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
}
- private int getCurrentLineTop(Spannable buffer, Layout layout) {
+ private static int getCurrentLineTop(Spannable buffer, Layout layout) {
return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
}
- private int getPageHeight(TextView widget) {
+ private static int getPageHeight(TextView widget) {
// This calculation does not take into account the view transformations that
// may have been applied to the child or its containers. In case of scaling or
// rotation, the calculated page height may be incorrect.
@@ -196,14 +196,16 @@
/** {@hide} */
@Override
protected boolean leftWord(TextView widget, Spannable buffer) {
- mWordIterator.setCharSequence(buffer);
+ final int selectionEnd = widget.getSelectionEnd();
+ mWordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer));
}
/** {@hide} */
@Override
protected boolean rightWord(TextView widget, Spannable buffer) {
- mWordIterator.setCharSequence(buffer);
+ final int selectionEnd = widget.getSelectionEnd();
+ mWordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer));
}
diff --git a/core/java/android/text/method/WordIterator.java b/core/java/android/text/method/WordIterator.java
index af524ee..239d9e8 100644
--- a/core/java/android/text/method/WordIterator.java
+++ b/core/java/android/text/method/WordIterator.java
@@ -17,14 +17,9 @@
package android.text.method;
-import android.text.CharSequenceIterator;
-import android.text.Editable;
import android.text.Selection;
-import android.text.Spanned;
-import android.text.TextWatcher;
import java.text.BreakIterator;
-import java.text.CharacterIterator;
import java.util.Locale;
/**
@@ -36,8 +31,11 @@
* {@hide}
*/
public class WordIterator implements Selection.PositionIterator {
- private CharSequence mCurrent;
- private boolean mCurrentDirty = false;
+ // Size of the window for the word iterator, should be greater than the longest word's length
+ private static final int WINDOW_WIDTH = 50;
+
+ private String mString;
+ private int mOffsetShift;
private BreakIterator mIterator;
@@ -56,70 +54,40 @@
mIterator = BreakIterator.getWordInstance(locale);
}
- private final TextWatcher mWatcher = new TextWatcher() {
- /** {@inheritDoc} */
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // ignored
- }
+ public void setCharSequence(CharSequence charSequence, int start, int end) {
+ mOffsetShift = Math.max(0, start - WINDOW_WIDTH);
+ final int windowEnd = Math.min(charSequence.length(), end + WINDOW_WIDTH);
- /** {@inheritDoc} */
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- mCurrentDirty = true;
- }
-
- /** {@inheritDoc} */
- public void afterTextChanged(Editable s) {
- // ignored
- }
- };
-
- public void setCharSequence(CharSequence incoming) {
- // When incoming is different object, move listeners to new sequence
- // and mark as dirty so we reload contents.
- if (mCurrent != incoming) {
- if (mCurrent instanceof Editable) {
- ((Editable) mCurrent).removeSpan(mWatcher);
- }
-
- if (incoming instanceof Editable) {
- ((Editable) incoming).setSpan(
- mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
- }
-
- mCurrent = incoming;
- mCurrentDirty = true;
- }
-
- if (mCurrentDirty) {
- final CharacterIterator charIterator = new CharSequenceIterator(mCurrent);
- mIterator.setText(charIterator);
-
- mCurrentDirty = false;
- }
+ mString = charSequence.toString().substring(mOffsetShift, windowEnd);
+ mIterator.setText(mString);
}
/** {@inheritDoc} */
public int preceding(int offset) {
+ int shiftedOffset = offset - mOffsetShift;
do {
- offset = mIterator.preceding(offset);
- if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) {
- break;
+ shiftedOffset = mIterator.preceding(shiftedOffset);
+ if (shiftedOffset == BreakIterator.DONE) {
+ return BreakIterator.DONE;
+ }
+ if (isOnLetterOrDigit(shiftedOffset)) {
+ return shiftedOffset + mOffsetShift;
}
} while (true);
-
- return offset;
}
/** {@inheritDoc} */
public int following(int offset) {
+ int shiftedOffset = offset - mOffsetShift;
do {
- offset = mIterator.following(offset);
- if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) {
- break;
+ shiftedOffset = mIterator.following(shiftedOffset);
+ if (shiftedOffset == BreakIterator.DONE) {
+ return BreakIterator.DONE;
+ }
+ if (isAfterLetterOrDigit(shiftedOffset)) {
+ return shiftedOffset + mOffsetShift;
}
} while (true);
-
- return offset;
}
/** If <code>offset</code> is within a word, returns the index of the first character of that
@@ -135,17 +103,18 @@
* @throws IllegalArgumentException is offset is not valid.
*/
public int getBeginning(int offset) {
- checkOffsetIsValid(offset);
+ final int shiftedOffset = offset - mOffsetShift;
+ checkOffsetIsValid(shiftedOffset);
- if (isOnLetterOrDigit(offset)) {
- if (mIterator.isBoundary(offset)) {
- return offset;
+ if (isOnLetterOrDigit(shiftedOffset)) {
+ if (mIterator.isBoundary(shiftedOffset)) {
+ return shiftedOffset + mOffsetShift;
} else {
- return mIterator.preceding(offset);
+ return mIterator.preceding(shiftedOffset) + mOffsetShift;
}
} else {
- if (isAfterLetterOrDigit(offset)) {
- return mIterator.preceding(offset);
+ if (isAfterLetterOrDigit(shiftedOffset)) {
+ return mIterator.preceding(shiftedOffset) + mOffsetShift;
}
}
return BreakIterator.DONE;
@@ -164,58 +133,44 @@
* @throws IllegalArgumentException is offset is not valid.
*/
public int getEnd(int offset) {
- checkOffsetIsValid(offset);
+ final int shiftedOffset = offset - mOffsetShift;
+ checkOffsetIsValid(shiftedOffset);
- if (isAfterLetterOrDigit(offset)) {
- if (mIterator.isBoundary(offset)) {
- return offset;
+ if (isAfterLetterOrDigit(shiftedOffset)) {
+ if (mIterator.isBoundary(shiftedOffset)) {
+ return shiftedOffset + mOffsetShift;
} else {
- return mIterator.following(offset);
+ return mIterator.following(shiftedOffset) + mOffsetShift;
}
} else {
- if (isOnLetterOrDigit(offset)) {
- return mIterator.following(offset);
+ if (isOnLetterOrDigit(shiftedOffset)) {
+ return mIterator.following(shiftedOffset) + mOffsetShift;
}
}
return BreakIterator.DONE;
}
- private boolean isAfterLetterOrDigit(int offset) {
- if (offset - 1 >= 0) {
- final char previousChar = mCurrent.charAt(offset - 1);
- if (Character.isLetterOrDigit(previousChar)) return true;
- if (offset - 2 >= 0) {
- final char previousPreviousChar = mCurrent.charAt(offset - 2);
- if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
- final int codePoint = Character.toCodePoint(previousPreviousChar, previousChar);
- return Character.isLetterOrDigit(codePoint);
- }
- }
+ private boolean isAfterLetterOrDigit(int shiftedOffset) {
+ if (shiftedOffset >= 1 && shiftedOffset <= mString.length()) {
+ final int codePoint = mString.codePointBefore(shiftedOffset);
+ if (Character.isLetterOrDigit(codePoint)) return true;
}
return false;
}
- private boolean isOnLetterOrDigit(int offset) {
- final int length = mCurrent.length();
- if (offset < length) {
- final char currentChar = mCurrent.charAt(offset);
- if (Character.isLetterOrDigit(currentChar)) return true;
- if (offset + 1 < length) {
- final char nextChar = mCurrent.charAt(offset + 1);
- if (Character.isSurrogatePair(currentChar, nextChar)) {
- final int codePoint = Character.toCodePoint(currentChar, nextChar);
- return Character.isLetterOrDigit(codePoint);
- }
- }
+ private boolean isOnLetterOrDigit(int shiftedOffset) {
+ if (shiftedOffset >= 0 && shiftedOffset < mString.length()) {
+ final int codePoint = mString.codePointAt(shiftedOffset);
+ if (Character.isLetterOrDigit(codePoint)) return true;
}
return false;
}
- private void checkOffsetIsValid(int offset) {
- if (offset < 0 || offset > mCurrent.length()) {
- final String message = "Invalid offset: " + offset +
- ". Valid range is [0, " + mCurrent.length() + "]";
- throw new IllegalArgumentException(message);
+ private void checkOffsetIsValid(int shiftedOffset) {
+ if (shiftedOffset < 0 || shiftedOffset > mString.length()) {
+ throw new IllegalArgumentException("Invalid offset: " + (shiftedOffset + mOffsetShift) +
+ ". Valid range is [" + mOffsetShift + ", " + (mString.length() + mOffsetShift) +
+ "]");
}
}
}
diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java
index ce17184..62b078f 100644
--- a/core/java/android/widget/SpellChecker.java
+++ b/core/java/android/widget/SpellChecker.java
@@ -20,6 +20,7 @@
import android.text.Editable;
import android.text.Selection;
import android.text.Spanned;
+import android.text.method.WordIterator;
import android.text.style.SpellCheckSpan;
import android.text.style.SuggestionSpan;
import android.view.textservice.SpellCheckerSession;
@@ -30,6 +31,8 @@
import com.android.internal.util.ArrayUtils;
+import java.text.BreakIterator;
+
/**
* Helper class for TextView. Bridge between the TextView and the Dictionnary service.
@@ -38,23 +41,30 @@
*/
public class SpellChecker implements SpellCheckerSessionListener {
+ private final static int MAX_SPELL_BATCH_SIZE = 50;
+
private final TextView mTextView;
+ private final Editable mText;
final SpellCheckerSession mSpellCheckerSession;
final int mCookie;
// 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.
+ // Contains null SpellCheckSpans after index mLength.
private int[] mIds;
private SpellCheckSpan[] mSpellCheckSpans;
// The mLength first elements of the above arrays have been initialized
private int mLength;
+ // Parsers on chunck of text, cutting text into words that will be checked
+ private SpellParser[] mSpellParsers = new SpellParser[0];
+
private int mSpanSequenceCounter = 0;
public SpellChecker(TextView textView) {
mTextView = textView;
+ mText = (Editable) textView.getText();
final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext().
getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
@@ -62,7 +72,7 @@
null /* not currently used by the textServicesManager */,
null /* null locale means use the languages defined in Settings
if referToSpellCheckerLanguageSettings is true */,
- this, true /* means use the languages defined in Settings */);
+ this, true /* means use the languages defined in Settings */);
mCookie = hashCode();
// Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand
@@ -76,7 +86,7 @@
* @return true if a spell checker session has successfully been created. Returns false if not,
* for instance when spell checking has been disabled in settings.
*/
- public boolean isSessionActive() {
+ private boolean isSessionActive() {
return mSpellCheckerSession != null;
}
@@ -84,6 +94,11 @@
if (mSpellCheckerSession != null) {
mSpellCheckerSession.close();
}
+
+ final int length = mSpellParsers.length;
+ for (int i = 0; i < length; i++) {
+ mSpellParsers[i].close();
+ }
}
private int nextSpellCheckSpanIndex() {
@@ -106,10 +121,9 @@
return mLength - 1;
}
- public void addSpellCheckSpan(int wordStart, int wordEnd) {
+ private void addSpellCheckSpan(int start, int end) {
final int index = nextSpellCheckSpanIndex();
- ((Editable) mTextView.getText()).setSpan(mSpellCheckSpans[index], wordStart, wordEnd,
- Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mText.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mIds[index] = mSpanSequenceCounter++;
}
@@ -127,12 +141,35 @@
spellCheck();
}
- public void spellCheck() {
+ public void spellCheck(int start, int end) {
+ if (!isSessionActive()) return;
+
+ final int length = mSpellParsers.length;
+ for (int i = 0; i < length; i++) {
+ final SpellParser spellParser = mSpellParsers[i];
+ if (spellParser.isDone()) {
+ spellParser.init(start, end);
+ spellParser.parse();
+ return;
+ }
+ }
+
+ // No available parser found in pool, create a new one
+ SpellParser[] newSpellParsers = new SpellParser[length + 1];
+ System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
+ mSpellParsers = newSpellParsers;
+
+ SpellParser spellParser = new SpellParser();
+ mSpellParsers[length] = spellParser;
+ spellParser.init(start, end);
+ spellParser.parse();
+ }
+
+ private void spellCheck() {
if (mSpellCheckerSession == null) return;
- final Editable editable = (Editable) mTextView.getText();
- final int selectionStart = Selection.getSelectionStart(editable);
- final int selectionEnd = Selection.getSelectionEnd(editable);
+ final int selectionStart = Selection.getSelectionStart(mText);
+ final int selectionEnd = Selection.getSelectionEnd(mText);
TextInfo[] textInfos = new TextInfo[mLength];
int textInfosCount = 0;
@@ -141,19 +178,19 @@
final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
if (spellCheckSpan.isSpellCheckInProgress()) continue;
- final int start = editable.getSpanStart(spellCheckSpan);
- final int end = editable.getSpanEnd(spellCheckSpan);
+ final int start = mText.getSpanStart(spellCheckSpan);
+ final int end = mText.getSpanEnd(spellCheckSpan);
// 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();
+ final String word = mText.subSequence(start, end).toString();
spellCheckSpan.setSpellCheckInProgress(true);
textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
}
}
if (textInfosCount > 0) {
- if (textInfosCount < mLength) {
+ if (textInfosCount < textInfos.length) {
TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
textInfos = textInfosCopy;
@@ -165,7 +202,6 @@
@Override
public void onGetSuggestions(SuggestionsInfo[] results) {
- final Editable editable = (Editable) mTextView.getText();
for (int i = 0; i < results.length; i++) {
SuggestionsInfo suggestionsInfo = results[i];
if (suggestionsInfo.getCookie() != mCookie) continue;
@@ -181,27 +217,35 @@
SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
if (!isInDictionary && looksLikeTypo) {
- createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan);
+ createMisspelledSuggestionSpan(suggestionsInfo, spellCheckSpan);
}
- editable.removeSpan(spellCheckSpan);
+ mText.removeSpan(spellCheckSpan);
break;
}
}
}
+
+ final int length = mSpellParsers.length;
+ for (int i = 0; i < length; i++) {
+ final SpellParser spellParser = mSpellParsers[i];
+ if (!spellParser.isDone()) {
+ spellParser.parse();
+ }
+ }
}
- private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
+ private void createMisspelledSuggestionSpan(SuggestionsInfo suggestionsInfo,
SpellCheckSpan spellCheckSpan) {
- final int start = editable.getSpanStart(spellCheckSpan);
- final int end = editable.getSpanEnd(spellCheckSpan);
+ final int start = mText.getSpanStart(spellCheckSpan);
+ final int end = mText.getSpanEnd(spellCheckSpan);
// Other suggestion spans may exist on that region, with identical suggestions, filter
// them out to avoid duplicates. First, filter suggestion spans on that exact region.
- SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class);
+ SuggestionSpan[] suggestionSpans = mText.getSpans(start, end, SuggestionSpan.class);
final int length = suggestionSpans.length;
for (int i = 0; i < length; i++) {
- final int spanStart = editable.getSpanStart(suggestionSpans[i]);
- final int spanEnd = editable.getSpanEnd(suggestionSpans[i]);
+ final int spanStart = mText.getSpanStart(suggestionSpans[i]);
+ final int spanEnd = mText.getSpanEnd(suggestionSpans[i]);
if (spanStart != start || spanEnd != end) {
suggestionSpans[i] = null;
break;
@@ -249,9 +293,132 @@
SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
- editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ mText.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// TODO limit to the word rectangle region
mTextView.invalidate();
}
+
+ private class SpellParser {
+ private WordIterator mWordIterator = new WordIterator(/*TODO Locale*/);
+ private Object mRange = new Object();
+
+ public void init(int start, int end) {
+ mText.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ public void close() {
+ mText.removeSpan(mRange);
+ }
+
+ public boolean isDone() {
+ return mText.getSpanStart(mRange) < 0;
+ }
+
+ public void parse() {
+ // Iterate over the newly added text and schedule new SpellCheckSpans
+ final int start = mText.getSpanStart(mRange);
+ final int end = mText.getSpanEnd(mRange);
+ mWordIterator.setCharSequence(mText, start, end);
+
+ // Move back to the beginning of the current word, if any
+ int wordStart = mWordIterator.preceding(start);
+ int wordEnd;
+ if (wordStart == BreakIterator.DONE) {
+ wordEnd = mWordIterator.following(start);
+ if (wordEnd != BreakIterator.DONE) {
+ wordStart = mWordIterator.getBeginning(wordEnd);
+ }
+ } else {
+ wordEnd = mWordIterator.getEnd(wordStart);
+ }
+ if (wordEnd == BreakIterator.DONE) {
+ mText.removeSpan(mRange);
+ 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 = mText.getSpans(start-1, end+1, SpellCheckSpan.class);
+ SuggestionSpan[] suggestionSpans = mText.getSpans(start-1, end+1, SuggestionSpan.class);
+
+ int nbWordsChecked = 0;
+ boolean scheduleOtherSpellCheck = false;
+
+ while (wordStart <= end) {
+ if (wordEnd >= start && wordEnd > wordStart) {
+ // A new word has been created across the interval boundaries with this edit.
+ // Previous spans (ended on start / started on end) removed, not valid anymore
+ if (wordStart < start && wordEnd > start) {
+ removeSpansAt(start, spellCheckSpans);
+ removeSpansAt(start, suggestionSpans);
+ }
+
+ if (wordStart < end && wordEnd > end) {
+ removeSpansAt(end, spellCheckSpans);
+ removeSpansAt(end, suggestionSpans);
+ }
+
+ // Do not create new boundary spans if they already exist
+ boolean createSpellCheckSpan = true;
+ if (wordEnd == start) {
+ for (int i = 0; i < spellCheckSpans.length; i++) {
+ final int spanEnd = mText.getSpanEnd(spellCheckSpans[i]);
+ if (spanEnd == start) {
+ createSpellCheckSpan = false;
+ break;
+ }
+ }
+ }
+
+ if (wordStart == end) {
+ for (int i = 0; i < spellCheckSpans.length; i++) {
+ final int spanStart = mText.getSpanStart(spellCheckSpans[i]);
+ if (spanStart == end) {
+ createSpellCheckSpan = false;
+ break;
+ }
+ }
+ }
+
+ if (createSpellCheckSpan) {
+ if (nbWordsChecked == MAX_SPELL_BATCH_SIZE) {
+ scheduleOtherSpellCheck = true;
+ break;
+ }
+ addSpellCheckSpan(wordStart, wordEnd);
+ nbWordsChecked++;
+ }
+ }
+
+ // iterate word by word
+ wordEnd = mWordIterator.following(wordEnd);
+ if (wordEnd == BreakIterator.DONE) break;
+ wordStart = mWordIterator.getBeginning(wordEnd);
+ if (wordStart == BreakIterator.DONE) {
+ break;
+ }
+ }
+
+ if (scheduleOtherSpellCheck) {
+ mText.setSpan(mRange, wordStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ mText.removeSpan(mRange);
+ }
+
+ spellCheck();
+ }
+
+ private <T> void removeSpansAt(int offset, T[] spans) {
+ final int length = spans.length;
+ for (int i = 0; i < length; i++) {
+ final T span = spans[i];
+ final int start = mText.getSpanStart(span);
+ if (start > offset) continue;
+ final int end = mText.getSpanEnd(span);
+ if (end < offset) continue;
+ mText.removeSpan(span);
+ }
+ }
+ }
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 68de2e9..41daf70 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -353,8 +353,6 @@
// Set when this TextView gained focus with some text selected. Will start selection mode.
private boolean mCreatedWithASelection = false;
- // Size of the window for the word iterator, should be greater than the longest word's length
- private static final int WORD_ITERATOR_WINDOW_WIDTH = 50;
private WordIterator mWordIterator;
private SpellChecker mSpellChecker;
@@ -6124,7 +6122,7 @@
* not the full view width with padding.
* {@hide}
*/
- protected void makeNewLayout(int w, int hintWidth,
+ protected void makeNewLayout(int wantWidth, int hintWidth,
BoringLayout.Metrics boring,
BoringLayout.Metrics hintBoring,
int ellipsisWidth, boolean bringIntoView) {
@@ -6136,8 +6134,8 @@
mHighlightPathBogus = true;
- if (w < 0) {
- w = 0;
+ if (wantWidth < 0) {
+ wantWidth = 0;
}
if (hintWidth < 0) {
hintWidth = 0;
@@ -6157,12 +6155,12 @@
resolveTextDirection();
}
- mLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment, shouldEllipsize,
+ mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
effectiveEllipsize, effectiveEllipsize == mEllipsize);
if (switchEllipsize) {
TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
TruncateAt.END : TruncateAt.MARQUEE;
- mSavedMarqueeModeLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment,
+ mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
}
@@ -6170,7 +6168,7 @@
mHintLayout = null;
if (mHint != null) {
- if (shouldEllipsize) hintWidth = w;
+ if (shouldEllipsize) hintWidth = wantWidth;
if (hintBoring == UNKNOWN_BORING) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
@@ -6254,12 +6252,12 @@
prepareCursorControllers();
}
- private Layout makeSingleLayout(int w, BoringLayout.Metrics boring, int ellipsisWidth,
+ private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
if (mText instanceof Spannable) {
- result = new DynamicLayout(mText, mTransformed, mTextPaint, w,
+ result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
ellipsisWidth);
@@ -6272,53 +6270,53 @@
}
if (boring != null) {
- if (boring.width <= w &&
+ if (boring.width <= wantWidth &&
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
- w, alignment, mSpacingMult, mSpacingAdd,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
- w, alignment, mSpacingMult, mSpacingAdd,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
}
if (useSaved) {
mSavedLayout = (BoringLayout) result;
}
- } else if (shouldEllipsize && boring.width <= w) {
+ } else if (shouldEllipsize && boring.width <= wantWidth) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
- w, alignment, mSpacingMult, mSpacingAdd,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
- w, alignment, mSpacingMult, mSpacingAdd,
+ wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
}
} else if (shouldEllipsize) {
result = new StaticLayout(mTransformed,
0, mTransformed.length(),
- mTextPaint, w, alignment, mTextDir, mSpacingMult,
+ mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, effectiveEllipsize,
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
} else {
result = new StaticLayout(mTransformed, mTextPaint,
- w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
+ wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
} else if (shouldEllipsize) {
result = new StaticLayout(mTransformed,
0, mTransformed.length(),
- mTextPaint, w, alignment, mTextDir, mSpacingMult,
+ mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, effectiveEllipsize,
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
} else {
result = new StaticLayout(mTransformed, mTextPaint,
- w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
+ wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
}
@@ -7749,98 +7747,8 @@
* Create new SpellCheckSpans on the modified region.
*/
private void updateSpellCheckSpans(int start, int end) {
- if (!isTextEditable() || !isSuggestionsEnabled() || !getSpellChecker().isSessionActive())
- return;
- Editable text = (Editable) mText;
-
- final int shift = prepareWordIterator(start, end);
- final int shiftedStart = start - shift;
- final int shiftedEnd = end - shift;
-
- // Move back to the beginning of the current word, if any
- int wordStart = mWordIterator.preceding(shiftedStart);
- int wordEnd;
- if (wordStart == BreakIterator.DONE) {
- wordEnd = mWordIterator.following(shiftedStart);
- if (wordEnd != BreakIterator.DONE) {
- wordStart = mWordIterator.getBeginning(wordEnd);
- }
- } else {
- wordEnd = mWordIterator.getEnd(wordStart);
- }
- if (wordEnd == BreakIterator.DONE) {
- 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 <= shiftedEnd) {
- if (wordEnd >= shiftedStart && wordEnd > wordStart) {
- // A new word has been created across the interval boundaries. Remove previous spans
- if (wordStart < shiftedStart && wordEnd > shiftedStart) {
- removeSpansAt(start, spellCheckSpans, text);
- removeSpansAt(start, suggestionSpans, text);
- }
-
- if (wordStart < shiftedEnd && wordEnd > shiftedEnd) {
- removeSpansAt(end, spellCheckSpans, text);
- removeSpansAt(end, suggestionSpans, text);
- }
-
- // Do not create new boundary spans if they already exist
- boolean createSpellCheckSpan = true;
- if (wordEnd == shiftedStart) {
- for (int i = 0; i < numberOfSpellCheckSpans; i++) {
- final int spanEnd = text.getSpanEnd(spellCheckSpans[i]);
- if (spanEnd == start) {
- createSpellCheckSpan = false;
- break;
- }
- }
- }
-
- if (wordStart == shiftedEnd) {
- for (int i = 0; i < numberOfSpellCheckSpans; i++) {
- final int spanStart = text.getSpanStart(spellCheckSpans[i]);
- if (spanStart == end) {
- createSpellCheckSpan = false;
- break;
- }
- }
- }
-
- if (createSpellCheckSpan) {
- mSpellChecker.addSpellCheckSpan(wordStart + shift, wordEnd + shift);
- }
- }
-
- // iterate word by word
- wordEnd = mWordIterator.following(wordEnd);
- if (wordEnd == BreakIterator.DONE) break;
- wordStart = mWordIterator.getBeginning(wordEnd);
- if (wordStart == BreakIterator.DONE) {
- Log.e(LOG_TAG, "No word beginning from " + (wordEnd + shift) + "in " + mText);
- break;
- }
- }
-
- mSpellChecker.spellCheck();
- }
-
- 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);
+ if (isTextEditable() && isSuggestionsEnabled()) {
+ getSpellChecker().spellCheck(start, end);
}
}
@@ -8930,15 +8838,16 @@
selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
} else {
- final int shift = prepareWordIterator(minOffset, maxOffset);
+ if (mWordIterator == null) {
+ mWordIterator = new WordIterator();
+ }
+ mWordIterator.setCharSequence(mText, minOffset, maxOffset);
- selectionStart = mWordIterator.getBeginning(minOffset - shift);
+ selectionStart = mWordIterator.getBeginning(minOffset);
if (selectionStart == BreakIterator.DONE) return false;
- selectionStart += shift;
- selectionEnd = mWordIterator.getEnd(maxOffset - shift);
+ selectionEnd = mWordIterator.getEnd(maxOffset);
if (selectionEnd == BreakIterator.DONE) return false;
- selectionEnd += shift;
if (selectionStart == selectionEnd) {
// Possible when the word iterator does not properly handle the text's language
@@ -8977,18 +8886,6 @@
return packRangeInLong(offset, offset);
}
- int prepareWordIterator(int start, int end) {
- if (mWordIterator == null) {
- mWordIterator = new WordIterator();
- }
-
- final int windowStart = Math.max(0, start - WORD_ITERATOR_WINDOW_WIDTH);
- final int windowEnd = Math.min(mText.length(), end + WORD_ITERATOR_WINDOW_WIDTH);
- mWordIterator.setCharSequence(mText.subSequence(windowStart, windowEnd));
-
- return windowStart;
- }
-
private SpellChecker getSpellChecker() {
if (mSpellChecker == null) {
mSpellChecker = new SpellChecker(this);