blob: b250414bf78a04afd6740ab5cda236806a97a941 [file] [log] [blame]
/*
* 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.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;
/**
* Walks through cursor positions at word boundaries. Internally uses
* {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence}
* for performance reasons.
*
* Also provides methods to determine word boundaries.
* {@hide}
*/
public class WordIterator implements Selection.PositionIterator {
private CharSequence mCurrent;
private boolean mCurrentDirty = false;
private BreakIterator mIterator;
/**
* Constructs a WordIterator using the default locale.
*/
public WordIterator() {
this(Locale.getDefault());
}
/**
* Constructs a new WordIterator for the specified locale.
* @param locale The locale to be used when analysing the text.
*/
public WordIterator(Locale locale) {
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);
}
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} */
public int preceding(int offset) {
do {
offset = mIterator.preceding(offset);
if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) {
break;
}
} while (true);
return offset;
}
/** {@inheritDoc} */
public int following(int offset) {
do {
offset = mIterator.following(offset);
if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) {
break;
}
} while (true);
return offset;
}
/** If <code>offset</code> is within a word, returns the index of the first character of that
* word, otherwise returns BreakIterator.DONE.
*
* The offsets that are considered to be part of a word are the indexes of its characters,
* <i>as well as</i> the index of its last character plus one.
* If offset is the index of a low surrogate character, BreakIterator.DONE will be returned.
*
* Valid range for offset is [0..textLength] (note the inclusive upper bound).
* The returned value is within [0..offset] or BreakIterator.DONE.
*
* @throws IllegalArgumentException is offset is not valid.
*/
public int getBeginning(int offset) {
checkOffsetIsValid(offset);
if (isOnLetterOrDigit(offset)) {
if (mIterator.isBoundary(offset)) {
return offset;
} else {
return mIterator.preceding(offset);
}
} else {
if (isAfterLetterOrDigit(offset)) {
return mIterator.preceding(offset);
}
}
return BreakIterator.DONE;
}
/** If <code>offset</code> is within a word, returns the index of the last character of that
* word plus one, otherwise returns BreakIterator.DONE.
*
* The offsets that are considered to be part of a word are the indexes of its characters,
* <i>as well as</i> the index of its last character plus one.
* If offset is the index of a low surrogate character, BreakIterator.DONE will be returned.
*
* Valid range for offset is [0..textLength] (note the inclusive upper bound).
* The returned value is within [offset..textLength] or BreakIterator.DONE.
*
* @throws IllegalArgumentException is offset is not valid.
*/
public int getEnd(int offset) {
checkOffsetIsValid(offset);
if (isAfterLetterOrDigit(offset)) {
if (mIterator.isBoundary(offset)) {
return offset;
} else {
return mIterator.following(offset);
}
} else {
if (isOnLetterOrDigit(offset)) {
return mIterator.following(offset);
}
}
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);
}
}
}
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);
}
}
}
return false;
}
private void checkOffsetIsValid(int offset) {
if (offset < 0 || offset > mCurrent.length()) {
final String message = "Valid range is [0, " + mCurrent.length() + "]";
throw new IllegalArgumentException(message);
}
}
}