Merge "Bug 5250788: LatinIME slows down as amount of Text increases"
This commit is contained in:
committed by
Android (Google) Code Review
commit
59ba2b2fab
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,11 +35,11 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
|
|||||||
(MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
|
(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)));
|
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
|
// 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
|
// may have been applied to the child or its containers. In case of scaling or
|
||||||
// rotation, the calculated page height may be incorrect.
|
// rotation, the calculated page height may be incorrect.
|
||||||
@@ -196,14 +196,16 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
|
|||||||
/** {@hide} */
|
/** {@hide} */
|
||||||
@Override
|
@Override
|
||||||
protected boolean leftWord(TextView widget, Spannable buffer) {
|
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));
|
return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@hide} */
|
/** {@hide} */
|
||||||
@Override
|
@Override
|
||||||
protected boolean rightWord(TextView widget, Spannable buffer) {
|
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));
|
return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,9 @@
|
|||||||
|
|
||||||
package android.text.method;
|
package android.text.method;
|
||||||
|
|
||||||
import android.text.CharSequenceIterator;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.Selection;
|
import android.text.Selection;
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
|
|
||||||
import java.text.BreakIterator;
|
import java.text.BreakIterator;
|
||||||
import java.text.CharacterIterator;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,8 +31,11 @@ import java.util.Locale;
|
|||||||
* {@hide}
|
* {@hide}
|
||||||
*/
|
*/
|
||||||
public class WordIterator implements Selection.PositionIterator {
|
public class WordIterator implements Selection.PositionIterator {
|
||||||
private CharSequence mCurrent;
|
// Size of the window for the word iterator, should be greater than the longest word's length
|
||||||
private boolean mCurrentDirty = false;
|
private static final int WINDOW_WIDTH = 50;
|
||||||
|
|
||||||
|
private String mString;
|
||||||
|
private int mOffsetShift;
|
||||||
|
|
||||||
private BreakIterator mIterator;
|
private BreakIterator mIterator;
|
||||||
|
|
||||||
@@ -56,70 +54,40 @@ public class WordIterator implements Selection.PositionIterator {
|
|||||||
mIterator = BreakIterator.getWordInstance(locale);
|
mIterator = BreakIterator.getWordInstance(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final TextWatcher mWatcher = new TextWatcher() {
|
public void setCharSequence(CharSequence charSequence, int start, int end) {
|
||||||
/** {@inheritDoc} */
|
mOffsetShift = Math.max(0, start - WINDOW_WIDTH);
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
final int windowEnd = Math.min(charSequence.length(), end + WINDOW_WIDTH);
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
mString = charSequence.toString().substring(mOffsetShift, windowEnd);
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
mIterator.setText(mString);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/** {@inheritDoc} */
|
||||||
public int preceding(int offset) {
|
public int preceding(int offset) {
|
||||||
|
int shiftedOffset = offset - mOffsetShift;
|
||||||
do {
|
do {
|
||||||
offset = mIterator.preceding(offset);
|
shiftedOffset = mIterator.preceding(shiftedOffset);
|
||||||
if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) {
|
if (shiftedOffset == BreakIterator.DONE) {
|
||||||
break;
|
return BreakIterator.DONE;
|
||||||
|
}
|
||||||
|
if (isOnLetterOrDigit(shiftedOffset)) {
|
||||||
|
return shiftedOffset + mOffsetShift;
|
||||||
}
|
}
|
||||||
} while (true);
|
} while (true);
|
||||||
|
|
||||||
return offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** {@inheritDoc} */
|
/** {@inheritDoc} */
|
||||||
public int following(int offset) {
|
public int following(int offset) {
|
||||||
|
int shiftedOffset = offset - mOffsetShift;
|
||||||
do {
|
do {
|
||||||
offset = mIterator.following(offset);
|
shiftedOffset = mIterator.following(shiftedOffset);
|
||||||
if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) {
|
if (shiftedOffset == BreakIterator.DONE) {
|
||||||
break;
|
return BreakIterator.DONE;
|
||||||
|
}
|
||||||
|
if (isAfterLetterOrDigit(shiftedOffset)) {
|
||||||
|
return shiftedOffset + mOffsetShift;
|
||||||
}
|
}
|
||||||
} while (true);
|
} while (true);
|
||||||
|
|
||||||
return offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** If <code>offset</code> is within a word, returns the index of the first character of that
|
/** 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.
|
* @throws IllegalArgumentException is offset is not valid.
|
||||||
*/
|
*/
|
||||||
public int getBeginning(int offset) {
|
public int getBeginning(int offset) {
|
||||||
checkOffsetIsValid(offset);
|
final int shiftedOffset = offset - mOffsetShift;
|
||||||
|
checkOffsetIsValid(shiftedOffset);
|
||||||
|
|
||||||
if (isOnLetterOrDigit(offset)) {
|
if (isOnLetterOrDigit(shiftedOffset)) {
|
||||||
if (mIterator.isBoundary(offset)) {
|
if (mIterator.isBoundary(shiftedOffset)) {
|
||||||
return offset;
|
return shiftedOffset + mOffsetShift;
|
||||||
} else {
|
} else {
|
||||||
return mIterator.preceding(offset);
|
return mIterator.preceding(shiftedOffset) + mOffsetShift;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isAfterLetterOrDigit(offset)) {
|
if (isAfterLetterOrDigit(shiftedOffset)) {
|
||||||
return mIterator.preceding(offset);
|
return mIterator.preceding(shiftedOffset) + mOffsetShift;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BreakIterator.DONE;
|
return BreakIterator.DONE;
|
||||||
@@ -164,58 +133,44 @@ public class WordIterator implements Selection.PositionIterator {
|
|||||||
* @throws IllegalArgumentException is offset is not valid.
|
* @throws IllegalArgumentException is offset is not valid.
|
||||||
*/
|
*/
|
||||||
public int getEnd(int offset) {
|
public int getEnd(int offset) {
|
||||||
checkOffsetIsValid(offset);
|
final int shiftedOffset = offset - mOffsetShift;
|
||||||
|
checkOffsetIsValid(shiftedOffset);
|
||||||
|
|
||||||
if (isAfterLetterOrDigit(offset)) {
|
if (isAfterLetterOrDigit(shiftedOffset)) {
|
||||||
if (mIterator.isBoundary(offset)) {
|
if (mIterator.isBoundary(shiftedOffset)) {
|
||||||
return offset;
|
return shiftedOffset + mOffsetShift;
|
||||||
} else {
|
} else {
|
||||||
return mIterator.following(offset);
|
return mIterator.following(shiftedOffset) + mOffsetShift;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isOnLetterOrDigit(offset)) {
|
if (isOnLetterOrDigit(shiftedOffset)) {
|
||||||
return mIterator.following(offset);
|
return mIterator.following(shiftedOffset) + mOffsetShift;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BreakIterator.DONE;
|
return BreakIterator.DONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAfterLetterOrDigit(int offset) {
|
private boolean isAfterLetterOrDigit(int shiftedOffset) {
|
||||||
if (offset - 1 >= 0) {
|
if (shiftedOffset >= 1 && shiftedOffset <= mString.length()) {
|
||||||
final char previousChar = mCurrent.charAt(offset - 1);
|
final int codePoint = mString.codePointBefore(shiftedOffset);
|
||||||
if (Character.isLetterOrDigit(previousChar)) return true;
|
if (Character.isLetterOrDigit(codePoint)) 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isOnLetterOrDigit(int offset) {
|
private boolean isOnLetterOrDigit(int shiftedOffset) {
|
||||||
final int length = mCurrent.length();
|
if (shiftedOffset >= 0 && shiftedOffset < mString.length()) {
|
||||||
if (offset < length) {
|
final int codePoint = mString.codePointAt(shiftedOffset);
|
||||||
final char currentChar = mCurrent.charAt(offset);
|
if (Character.isLetterOrDigit(codePoint)) return true;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkOffsetIsValid(int offset) {
|
private void checkOffsetIsValid(int shiftedOffset) {
|
||||||
if (offset < 0 || offset > mCurrent.length()) {
|
if (shiftedOffset < 0 || shiftedOffset > mString.length()) {
|
||||||
final String message = "Invalid offset: " + offset +
|
throw new IllegalArgumentException("Invalid offset: " + (shiftedOffset + mOffsetShift) +
|
||||||
". Valid range is [0, " + mCurrent.length() + "]";
|
". Valid range is [" + mOffsetShift + ", " + (mString.length() + mOffsetShift) +
|
||||||
throw new IllegalArgumentException(message);
|
"]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import android.content.Context;
|
|||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.Selection;
|
import android.text.Selection;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.method.WordIterator;
|
||||||
import android.text.style.SpellCheckSpan;
|
import android.text.style.SpellCheckSpan;
|
||||||
import android.text.style.SuggestionSpan;
|
import android.text.style.SuggestionSpan;
|
||||||
import android.view.textservice.SpellCheckerSession;
|
import android.view.textservice.SpellCheckerSession;
|
||||||
@@ -30,6 +31,8 @@ import android.view.textservice.TextServicesManager;
|
|||||||
|
|
||||||
import com.android.internal.util.ArrayUtils;
|
import com.android.internal.util.ArrayUtils;
|
||||||
|
|
||||||
|
import java.text.BreakIterator;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class for TextView. Bridge between the TextView and the Dictionnary service.
|
* 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 {
|
public class SpellChecker implements SpellCheckerSessionListener {
|
||||||
|
|
||||||
|
private final static int MAX_SPELL_BATCH_SIZE = 50;
|
||||||
|
|
||||||
private final TextView mTextView;
|
private final TextView mTextView;
|
||||||
|
private final Editable mText;
|
||||||
|
|
||||||
final SpellCheckerSession mSpellCheckerSession;
|
final SpellCheckerSession mSpellCheckerSession;
|
||||||
final int mCookie;
|
final int mCookie;
|
||||||
|
|
||||||
// Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
|
// Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
|
||||||
// SpellCheckSpan has been recycled and can be-reused.
|
// 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 int[] mIds;
|
||||||
private SpellCheckSpan[] mSpellCheckSpans;
|
private SpellCheckSpan[] mSpellCheckSpans;
|
||||||
// The mLength first elements of the above arrays have been initialized
|
// The mLength first elements of the above arrays have been initialized
|
||||||
private int mLength;
|
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;
|
private int mSpanSequenceCounter = 0;
|
||||||
|
|
||||||
public SpellChecker(TextView textView) {
|
public SpellChecker(TextView textView) {
|
||||||
mTextView = textView;
|
mTextView = textView;
|
||||||
|
mText = (Editable) textView.getText();
|
||||||
|
|
||||||
final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext().
|
final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext().
|
||||||
getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
|
getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
|
||||||
@@ -62,7 +72,7 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
null /* not currently used by the textServicesManager */,
|
null /* not currently used by the textServicesManager */,
|
||||||
null /* null locale means use the languages defined in Settings
|
null /* null locale means use the languages defined in Settings
|
||||||
if referToSpellCheckerLanguageSettings is true */,
|
if referToSpellCheckerLanguageSettings is true */,
|
||||||
this, true /* means use the languages defined in Settings */);
|
this, true /* means use the languages defined in Settings */);
|
||||||
mCookie = hashCode();
|
mCookie = hashCode();
|
||||||
|
|
||||||
// Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand
|
// 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,
|
* @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.
|
* for instance when spell checking has been disabled in settings.
|
||||||
*/
|
*/
|
||||||
public boolean isSessionActive() {
|
private boolean isSessionActive() {
|
||||||
return mSpellCheckerSession != null;
|
return mSpellCheckerSession != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +94,11 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
if (mSpellCheckerSession != null) {
|
if (mSpellCheckerSession != null) {
|
||||||
mSpellCheckerSession.close();
|
mSpellCheckerSession.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final int length = mSpellParsers.length;
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
mSpellParsers[i].close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int nextSpellCheckSpanIndex() {
|
private int nextSpellCheckSpanIndex() {
|
||||||
@@ -106,10 +121,9 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
return mLength - 1;
|
return mLength - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSpellCheckSpan(int wordStart, int wordEnd) {
|
private void addSpellCheckSpan(int start, int end) {
|
||||||
final int index = nextSpellCheckSpanIndex();
|
final int index = nextSpellCheckSpanIndex();
|
||||||
((Editable) mTextView.getText()).setSpan(mSpellCheckSpans[index], wordStart, wordEnd,
|
mText.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
mIds[index] = mSpanSequenceCounter++;
|
mIds[index] = mSpanSequenceCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,12 +141,35 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
spellCheck();
|
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;
|
if (mSpellCheckerSession == null) return;
|
||||||
|
|
||||||
final Editable editable = (Editable) mTextView.getText();
|
final int selectionStart = Selection.getSelectionStart(mText);
|
||||||
final int selectionStart = Selection.getSelectionStart(editable);
|
final int selectionEnd = Selection.getSelectionEnd(mText);
|
||||||
final int selectionEnd = Selection.getSelectionEnd(editable);
|
|
||||||
|
|
||||||
TextInfo[] textInfos = new TextInfo[mLength];
|
TextInfo[] textInfos = new TextInfo[mLength];
|
||||||
int textInfosCount = 0;
|
int textInfosCount = 0;
|
||||||
@@ -141,19 +178,19 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
|
final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
|
||||||
if (spellCheckSpan.isSpellCheckInProgress()) continue;
|
if (spellCheckSpan.isSpellCheckInProgress()) continue;
|
||||||
|
|
||||||
final int start = editable.getSpanStart(spellCheckSpan);
|
final int start = mText.getSpanStart(spellCheckSpan);
|
||||||
final int end = editable.getSpanEnd(spellCheckSpan);
|
final int end = mText.getSpanEnd(spellCheckSpan);
|
||||||
|
|
||||||
// Do not check this word if the user is currently editing it
|
// Do not check this word if the user is currently editing it
|
||||||
if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) {
|
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);
|
spellCheckSpan.setSpellCheckInProgress(true);
|
||||||
textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
|
textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textInfosCount > 0) {
|
if (textInfosCount > 0) {
|
||||||
if (textInfosCount < mLength) {
|
if (textInfosCount < textInfos.length) {
|
||||||
TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
|
TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
|
||||||
System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
|
System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
|
||||||
textInfos = textInfosCopy;
|
textInfos = textInfosCopy;
|
||||||
@@ -165,7 +202,6 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onGetSuggestions(SuggestionsInfo[] results) {
|
public void onGetSuggestions(SuggestionsInfo[] results) {
|
||||||
final Editable editable = (Editable) mTextView.getText();
|
|
||||||
for (int i = 0; i < results.length; i++) {
|
for (int i = 0; i < results.length; i++) {
|
||||||
SuggestionsInfo suggestionsInfo = results[i];
|
SuggestionsInfo suggestionsInfo = results[i];
|
||||||
if (suggestionsInfo.getCookie() != mCookie) continue;
|
if (suggestionsInfo.getCookie() != mCookie) continue;
|
||||||
@@ -181,27 +217,35 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
|
|
||||||
SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
|
SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
|
||||||
if (!isInDictionary && looksLikeTypo) {
|
if (!isInDictionary && looksLikeTypo) {
|
||||||
createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan);
|
createMisspelledSuggestionSpan(suggestionsInfo, spellCheckSpan);
|
||||||
}
|
}
|
||||||
editable.removeSpan(spellCheckSpan);
|
mText.removeSpan(spellCheckSpan);
|
||||||
break;
|
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) {
|
SpellCheckSpan spellCheckSpan) {
|
||||||
final int start = editable.getSpanStart(spellCheckSpan);
|
final int start = mText.getSpanStart(spellCheckSpan);
|
||||||
final int end = editable.getSpanEnd(spellCheckSpan);
|
final int end = mText.getSpanEnd(spellCheckSpan);
|
||||||
|
|
||||||
// Other suggestion spans may exist on that region, with identical suggestions, filter
|
// 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.
|
// 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;
|
final int length = suggestionSpans.length;
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
final int spanStart = editable.getSpanStart(suggestionSpans[i]);
|
final int spanStart = mText.getSpanStart(suggestionSpans[i]);
|
||||||
final int spanEnd = editable.getSpanEnd(suggestionSpans[i]);
|
final int spanEnd = mText.getSpanEnd(suggestionSpans[i]);
|
||||||
if (spanStart != start || spanEnd != end) {
|
if (spanStart != start || spanEnd != end) {
|
||||||
suggestionSpans[i] = null;
|
suggestionSpans[i] = null;
|
||||||
break;
|
break;
|
||||||
@@ -249,9 +293,132 @@ public class SpellChecker implements SpellCheckerSessionListener {
|
|||||||
|
|
||||||
SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
|
SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
|
||||||
SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
|
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
|
// TODO limit to the word rectangle region
|
||||||
mTextView.invalidate();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
// Set when this TextView gained focus with some text selected. Will start selection mode.
|
||||||
private boolean mCreatedWithASelection = false;
|
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 WordIterator mWordIterator;
|
||||||
|
|
||||||
private SpellChecker mSpellChecker;
|
private SpellChecker mSpellChecker;
|
||||||
@@ -6124,7 +6122,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
* not the full view width with padding.
|
* not the full view width with padding.
|
||||||
* {@hide}
|
* {@hide}
|
||||||
*/
|
*/
|
||||||
protected void makeNewLayout(int w, int hintWidth,
|
protected void makeNewLayout(int wantWidth, int hintWidth,
|
||||||
BoringLayout.Metrics boring,
|
BoringLayout.Metrics boring,
|
||||||
BoringLayout.Metrics hintBoring,
|
BoringLayout.Metrics hintBoring,
|
||||||
int ellipsisWidth, boolean bringIntoView) {
|
int ellipsisWidth, boolean bringIntoView) {
|
||||||
@@ -6136,8 +6134,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
|
|
||||||
mHighlightPathBogus = true;
|
mHighlightPathBogus = true;
|
||||||
|
|
||||||
if (w < 0) {
|
if (wantWidth < 0) {
|
||||||
w = 0;
|
wantWidth = 0;
|
||||||
}
|
}
|
||||||
if (hintWidth < 0) {
|
if (hintWidth < 0) {
|
||||||
hintWidth = 0;
|
hintWidth = 0;
|
||||||
@@ -6157,12 +6155,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
resolveTextDirection();
|
resolveTextDirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
mLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment, shouldEllipsize,
|
mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
|
||||||
effectiveEllipsize, effectiveEllipsize == mEllipsize);
|
effectiveEllipsize, effectiveEllipsize == mEllipsize);
|
||||||
if (switchEllipsize) {
|
if (switchEllipsize) {
|
||||||
TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
|
TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
|
||||||
TruncateAt.END : TruncateAt.MARQUEE;
|
TruncateAt.END : TruncateAt.MARQUEE;
|
||||||
mSavedMarqueeModeLayout = makeSingleLayout(w, boring, ellipsisWidth, alignment,
|
mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
|
||||||
shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
|
shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6170,7 +6168,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
mHintLayout = null;
|
mHintLayout = null;
|
||||||
|
|
||||||
if (mHint != null) {
|
if (mHint != null) {
|
||||||
if (shouldEllipsize) hintWidth = w;
|
if (shouldEllipsize) hintWidth = wantWidth;
|
||||||
|
|
||||||
if (hintBoring == UNKNOWN_BORING) {
|
if (hintBoring == UNKNOWN_BORING) {
|
||||||
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
|
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
|
||||||
@@ -6254,12 +6252,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
prepareCursorControllers();
|
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,
|
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
|
||||||
boolean useSaved) {
|
boolean useSaved) {
|
||||||
Layout result = null;
|
Layout result = null;
|
||||||
if (mText instanceof Spannable) {
|
if (mText instanceof Spannable) {
|
||||||
result = new DynamicLayout(mText, mTransformed, mTextPaint, w,
|
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
|
||||||
alignment, mTextDir, mSpacingMult,
|
alignment, mTextDir, mSpacingMult,
|
||||||
mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
|
mSpacingAdd, mIncludePad, mInput == null ? effectiveEllipsize : null,
|
||||||
ellipsisWidth);
|
ellipsisWidth);
|
||||||
@@ -6272,53 +6270,53 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (boring != null) {
|
if (boring != null) {
|
||||||
if (boring.width <= w &&
|
if (boring.width <= wantWidth &&
|
||||||
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
|
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
|
||||||
if (useSaved && mSavedLayout != null) {
|
if (useSaved && mSavedLayout != null) {
|
||||||
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
|
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
|
||||||
w, alignment, mSpacingMult, mSpacingAdd,
|
wantWidth, alignment, mSpacingMult, mSpacingAdd,
|
||||||
boring, mIncludePad);
|
boring, mIncludePad);
|
||||||
} else {
|
} else {
|
||||||
result = BoringLayout.make(mTransformed, mTextPaint,
|
result = BoringLayout.make(mTransformed, mTextPaint,
|
||||||
w, alignment, mSpacingMult, mSpacingAdd,
|
wantWidth, alignment, mSpacingMult, mSpacingAdd,
|
||||||
boring, mIncludePad);
|
boring, mIncludePad);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useSaved) {
|
if (useSaved) {
|
||||||
mSavedLayout = (BoringLayout) result;
|
mSavedLayout = (BoringLayout) result;
|
||||||
}
|
}
|
||||||
} else if (shouldEllipsize && boring.width <= w) {
|
} else if (shouldEllipsize && boring.width <= wantWidth) {
|
||||||
if (useSaved && mSavedLayout != null) {
|
if (useSaved && mSavedLayout != null) {
|
||||||
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
|
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
|
||||||
w, alignment, mSpacingMult, mSpacingAdd,
|
wantWidth, alignment, mSpacingMult, mSpacingAdd,
|
||||||
boring, mIncludePad, effectiveEllipsize,
|
boring, mIncludePad, effectiveEllipsize,
|
||||||
ellipsisWidth);
|
ellipsisWidth);
|
||||||
} else {
|
} else {
|
||||||
result = BoringLayout.make(mTransformed, mTextPaint,
|
result = BoringLayout.make(mTransformed, mTextPaint,
|
||||||
w, alignment, mSpacingMult, mSpacingAdd,
|
wantWidth, alignment, mSpacingMult, mSpacingAdd,
|
||||||
boring, mIncludePad, effectiveEllipsize,
|
boring, mIncludePad, effectiveEllipsize,
|
||||||
ellipsisWidth);
|
ellipsisWidth);
|
||||||
}
|
}
|
||||||
} else if (shouldEllipsize) {
|
} else if (shouldEllipsize) {
|
||||||
result = new StaticLayout(mTransformed,
|
result = new StaticLayout(mTransformed,
|
||||||
0, mTransformed.length(),
|
0, mTransformed.length(),
|
||||||
mTextPaint, w, alignment, mTextDir, mSpacingMult,
|
mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
|
||||||
mSpacingAdd, mIncludePad, effectiveEllipsize,
|
mSpacingAdd, mIncludePad, effectiveEllipsize,
|
||||||
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
|
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
|
||||||
} else {
|
} else {
|
||||||
result = new StaticLayout(mTransformed, mTextPaint,
|
result = new StaticLayout(mTransformed, mTextPaint,
|
||||||
w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
|
wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
|
||||||
mIncludePad);
|
mIncludePad);
|
||||||
}
|
}
|
||||||
} else if (shouldEllipsize) {
|
} else if (shouldEllipsize) {
|
||||||
result = new StaticLayout(mTransformed,
|
result = new StaticLayout(mTransformed,
|
||||||
0, mTransformed.length(),
|
0, mTransformed.length(),
|
||||||
mTextPaint, w, alignment, mTextDir, mSpacingMult,
|
mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
|
||||||
mSpacingAdd, mIncludePad, effectiveEllipsize,
|
mSpacingAdd, mIncludePad, effectiveEllipsize,
|
||||||
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
|
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
|
||||||
} else {
|
} else {
|
||||||
result = new StaticLayout(mTransformed, mTextPaint,
|
result = new StaticLayout(mTransformed, mTextPaint,
|
||||||
w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
|
wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
|
||||||
mIncludePad);
|
mIncludePad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7749,98 +7747,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
* Create new SpellCheckSpans on the modified region.
|
* Create new SpellCheckSpans on the modified region.
|
||||||
*/
|
*/
|
||||||
private void updateSpellCheckSpans(int start, int end) {
|
private void updateSpellCheckSpans(int start, int end) {
|
||||||
if (!isTextEditable() || !isSuggestionsEnabled() || !getSpellChecker().isSessionActive())
|
if (isTextEditable() && isSuggestionsEnabled()) {
|
||||||
return;
|
getSpellChecker().spellCheck(start, end);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8930,15 +8838,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|
|||||||
selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
|
selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
|
||||||
selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
|
selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
|
||||||
} else {
|
} 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;
|
if (selectionStart == BreakIterator.DONE) return false;
|
||||||
selectionStart += shift;
|
|
||||||
|
|
||||||
selectionEnd = mWordIterator.getEnd(maxOffset - shift);
|
selectionEnd = mWordIterator.getEnd(maxOffset);
|
||||||
if (selectionEnd == BreakIterator.DONE) return false;
|
if (selectionEnd == BreakIterator.DONE) return false;
|
||||||
selectionEnd += shift;
|
|
||||||
|
|
||||||
if (selectionStart == selectionEnd) {
|
if (selectionStart == selectionEnd) {
|
||||||
// Possible when the word iterator does not properly handle the text's language
|
// 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);
|
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() {
|
private SpellChecker getSpellChecker() {
|
||||||
if (mSpellChecker == null) {
|
if (mSpellChecker == null) {
|
||||||
mSpellChecker = new SpellChecker(this);
|
mSpellChecker = new SpellChecker(this);
|
||||||
|
|||||||
Reference in New Issue
Block a user