diff options
-rw-r--r-- | core/java/android/text/CharSequenceIterator.java | 100 | ||||
-rw-r--r-- | core/java/android/text/method/ArrowKeyMovementMethod.java | 10 | ||||
-rw-r--r-- | core/java/android/text/method/WordIterator.java | 151 | ||||
-rw-r--r-- | core/java/android/widget/SpellChecker.java | 215 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 155 |
5 files changed, 276 insertions, 355 deletions
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 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme (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 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme /** {@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 @@ import java.util.Locale; * {@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 @@ public class WordIterator implements Selection.PositionIterator { mIterator = BreakIterator.getWordInstance(locale); } - private final TextWatcher mWatcher = new TextWatcher() { - /** {@inheritDoc} */ - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - // ignored - } - - /** {@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); - } + 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); - 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 @@ public class WordIterator implements Selection.PositionIterator { * @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 @@ public class WordIterator implements Selection.PositionIterator { * @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.content.Context; 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 android.view.textservice.TextServicesManager; 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 @@ import com.android.internal.util.ArrayUtils; */ 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 @@ public class SpellChecker implements SpellCheckerSessionListener { 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 @@ public class SpellChecker implements SpellCheckerSessionListener { * @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 @@ public class SpellChecker implements SpellCheckerSessionListener { 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 @@ public class SpellChecker implements SpellCheckerSessionListener { 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 @@ public class SpellChecker implements SpellCheckerSessionListener { 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 @@ public class SpellChecker implements SpellCheckerSessionListener { 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 @@ public class SpellChecker implements SpellCheckerSessionListener { @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 @@ public class SpellChecker implements SpellCheckerSessionListener { 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 @@ public class SpellChecker implements SpellCheckerSessionListener { 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mHighlightPathBogus = true; - if (w < 0) { - w = 0; + if (wantWidth < 0) { + wantWidth = 0; } if (hintWidth < 0) { hintWidth = 0; @@ -6157,12 +6155,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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); |