summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
authorGilles Debunne <debunne@google.com>2011-04-27 16:01:30 -0700
committerAndroid (Google) Code Review <android-gerrit@google.com>2011-04-27 16:01:30 -0700
commit0d846898ba2a13d016573b795a88ed8f568ace35 (patch)
tree2990a28912491e298e69d56e1117c3af793c0067 /core/java
parentabe4b55f2872b40b116693ac7c51801ccfc40ece (diff)
parent214a8627c5688b7d942a1209fa38eda4a24a7240 (diff)
downloadframeworks_base-0d846898ba2a13d016573b795a88ed8f568ace35.zip
frameworks_base-0d846898ba2a13d016573b795a88ed8f568ace35.tar.gz
frameworks_base-0d846898ba2a13d016573b795a88ed8f568ace35.tar.bz2
Merge "Support for overlapping spans in TextView's suggestions."
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/widget/TextView.java211
1 files changed, 201 insertions, 10 deletions
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 4d3aa68..6c5d117 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -60,6 +60,7 @@ import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.StaticLayout;
@@ -80,10 +81,13 @@ import android.text.method.SingleLineTransformationMethod;
import android.text.method.TextKeyListener;
import android.text.method.TimeKeyListener;
import android.text.method.TransformationMethod;
+import android.text.method.WordIterator;
import android.text.style.ClickableSpan;
import android.text.style.ParagraphStyle;
import android.text.style.SuggestionSpan;
+import android.text.style.TextAppearanceSpan;
import android.text.style.URLSpan;
+import android.text.style.UnderlineSpan;
import android.text.style.UpdateAppearance;
import android.text.util.Linkify;
import android.util.AttributeSet;
@@ -127,6 +131,7 @@ import android.widget.RemoteViews.RemoteView;
import java.io.IOException;
import java.lang.ref.WeakReference;
+import java.text.BreakIterator;
import java.util.ArrayList;
/**
@@ -314,6 +319,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout;
private int mTextEditSuggestionItemLayout;
private SuggestionsPopupWindow mSuggestionsPopupWindow;
+ private SuggestionRangeSpan mSuggestionRangeSpan;
private int mCursorDrawableRes;
private final Drawable[] mCursorDrawable = new Drawable[2];
@@ -8225,13 +8231,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
}
+ private static class SuggestionRangeSpan extends UnderlineSpan {
+ // TODO themable, would be nice to make it a child class of TextAppearanceSpan, but
+ // there is no way to have underline and TextAppearanceSpan.
+ }
+
private class SuggestionsPopupWindow implements OnClickListener {
private static final int MAX_NUMBER_SUGGESTIONS = 5;
- private static final long NO_SUGGESTIONS = -1L;
+ private static final int NO_SUGGESTIONS = -1;
private final PopupWindow mContainer;
private final ViewGroup[] mSuggestionViews = new ViewGroup[2];
private final int[] mSuggestionViewLayouts = new int[] {
mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout};
+ private WordIterator mWordIterator;
+ private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0];
public SuggestionsPopupWindow() {
mContainer = new PopupWindow(TextView.this.mContext, null,
@@ -8244,6 +8257,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
}
+ private class SuggestionInfo {
+ int suggestionStart, suggestionEnd; // range of suggestion item with replacement text
+ int spanStart, spanEnd; // range in TextView where text should be inserted
+ }
+
private ViewGroup getViewGroup(boolean under) {
final int viewIndex = under ? 0 : 1;
ViewGroup viewGroup = mSuggestionViews[viewIndex];
@@ -8277,6 +8295,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
"Inflated TextEdit suggestion item is not a TextView: " + childView);
}
+ childView.setTag(new SuggestionInfo());
viewGroup.addView(childView);
childView.setOnClickListener(this);
}
@@ -8299,21 +8318,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
mContainer.setContentView(viewGroup);
int totalNbSuggestions = 0;
+ int spanUnionStart = mText.length();
+ int spanUnionEnd = 0;
+
for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
final int spanStart = spannable.getSpanStart(suggestionSpan);
final int spanEnd = spannable.getSpanEnd(suggestionSpan);
- final Long spanRange = packRangeInLong(spanStart, spanEnd);
+ spanUnionStart = Math.min(spanStart, spanUnionStart);
+ spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
String[] suggestions = suggestionSpan.getSuggestions();
int nbSuggestions = suggestions.length;
for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions);
textView.setText(suggestions[suggestionIndex]);
- textView.setTag(spanRange);
+ SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
+ suggestionInfo.spanStart = spanStart;
+ suggestionInfo.spanEnd = spanEnd;
totalNbSuggestions++;
- if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) {
+ if (totalNbSuggestions > MAX_NUMBER_SUGGESTIONS) {
+ // Also end outer for loop
spanIndex = nbSpans;
break;
}
@@ -8324,8 +8350,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// TODO Replace by final text, use a dedicated layout, add a fade out timer...
TextView textView = (TextView) viewGroup.getChildAt(0);
textView.setText("No suggestions available");
- textView.setTag(NO_SUGGESTIONS);
+ SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
+ suggestionInfo.spanStart = NO_SUGGESTIONS;
totalNbSuggestions++;
+ } else {
+ if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
+ ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ for (int i = 0; i < totalNbSuggestions; i++) {
+ final TextView textView = (TextView) viewGroup.getChildAt(i);
+ highlightTextDifferences(textView, spanUnionStart, spanUnionEnd);
+ }
}
for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) {
@@ -8338,7 +8374,158 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
positionAtCursor();
}
+ private long[] getWordLimits(CharSequence text) {
+ if (mWordIterator == null) mWordIterator = new WordIterator(); // TODO locale
+ mWordIterator.setCharSequence(text);
+
+ // First pass will simply count the number of words to be able to create an array
+ // Not too expensive since previous break positions are cached by the BreakIterator
+ int nbWords = 0;
+ int position = mWordIterator.following(0);
+ while (position != BreakIterator.DONE) {
+ nbWords++;
+ position = mWordIterator.following(position);
+ }
+
+ int index = 0;
+ long[] result = new long[nbWords];
+
+ position = mWordIterator.following(0);
+ while (position != BreakIterator.DONE) {
+ int wordStart = mWordIterator.getBeginning(position);
+ result[index++] = packRangeInLong(wordStart, position);
+ position = mWordIterator.following(position);
+ }
+
+ return result;
+ }
+
+ private TextAppearanceSpan highlightSpan(int index) {
+ final int length = mHighlightSpans.length;
+ if (index < length) {
+ return mHighlightSpans[index];
+ }
+
+ // Assumes indexes are requested in sequence: simply append one more item
+ TextAppearanceSpan[] newArray = new TextAppearanceSpan[length + 1];
+ System.arraycopy(mHighlightSpans, 0, newArray, 0, length);
+ TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
+ android.R.style.TextAppearance_SuggestionHighlight);
+ newArray[length] = highlightSpan;
+ mHighlightSpans = newArray;
+ return highlightSpan;
+ }
+
+ private void highlightTextDifferences(TextView textView, int unionStart, int unionEnd) {
+ SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
+ final int spanStart = suggestionInfo.spanStart;
+ final int spanEnd = suggestionInfo.spanEnd;
+
+ // Remove all text formating by converting to Strings
+ final String text = textView.getText().toString();
+ final String sourceText = mText.subSequence(spanStart, spanEnd).toString();
+
+ long[] sourceWordLimits = getWordLimits(sourceText);
+ long[] wordLimits = getWordLimits(text);
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder();
+ // span [spanStart, spanEnd] is included in union [spanUnionStart, int spanUnionEnd]
+ // The final result is made of 3 parts: the text before, between and after the span
+ // This is the text before, provided for context
+ ssb.append(mText.subSequence(unionStart, spanStart).toString());
+
+ // shift is used to offset spans positions wrt span's beginning
+ final int shift = spanStart - unionStart;
+ suggestionInfo.suggestionStart = shift;
+ suggestionInfo.suggestionEnd = shift + text.length();
+
+ // This is the actual suggestion text, which will be highlighted by the following code
+ ssb.append(text);
+
+ String[] words = new String[wordLimits.length];
+ for (int i = 0; i < wordLimits.length; i++) {
+ int wordStart = extractRangeStartFromLong(wordLimits[i]);
+ int wordEnd = extractRangeEndFromLong(wordLimits[i]);
+ words[i] = text.substring(wordStart, wordEnd);
+ }
+
+ // Highlighted word algorithm is bases on word matching between source and text
+ // Matching words are found from left to right. TODO: change for RTL languages
+ // Characters between matching words are highlighted
+ int previousCommonWordIndex = -1;
+ int nbHighlightSpans = 0;
+ for (int i = 0; i < sourceWordLimits.length; i++) {
+ int wordStart = extractRangeStartFromLong(sourceWordLimits[i]);
+ int wordEnd = extractRangeEndFromLong(sourceWordLimits[i]);
+ String sourceWord = sourceText.substring(wordStart, wordEnd);
+
+ for (int j = previousCommonWordIndex + 1; j < words.length; j++) {
+ if (sourceWord.equals(words[j])) {
+ if (j != previousCommonWordIndex + 1) {
+ int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 :
+ extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
+ int lastDifferentPosition = extractRangeStartFromLong(wordLimits[j]);
+ ssb.setSpan(highlightSpan(nbHighlightSpans++),
+ shift + firstDifferentPosition, shift + lastDifferentPosition,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ // Compare characters between words
+ int previousSourceWordEnd = i == 0 ? 0 :
+ extractRangeEndFromLong(sourceWordLimits[i - 1]);
+ int sourceWordStart = extractRangeStartFromLong(sourceWordLimits[i]);
+ String sourceSpaces = sourceText.substring(previousSourceWordEnd,
+ sourceWordStart);
+
+ int previousWordEnd = j == 0 ? 0 :
+ extractRangeEndFromLong(wordLimits[j - 1]);
+ int currentWordStart = extractRangeStartFromLong(wordLimits[j]);
+ String textSpaces = text.substring(previousWordEnd, currentWordStart);
+
+ if (!sourceSpaces.equals(textSpaces)) {
+ ssb.setSpan(highlightSpan(nbHighlightSpans++),
+ shift + previousWordEnd, shift + currentWordStart,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ previousCommonWordIndex = j;
+ break;
+ }
+ }
+ }
+
+ // Finally, compare ends of Strings
+ if (previousCommonWordIndex < words.length - 1) {
+ int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 :
+ extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
+ int lastDifferentPosition = textView.length();
+ ssb.setSpan(highlightSpan(nbHighlightSpans++),
+ shift + firstDifferentPosition, shift + lastDifferentPosition,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } else {
+ int lastSourceWordEnd = sourceWordLimits.length == 0 ? 0 :
+ extractRangeEndFromLong(sourceWordLimits[sourceWordLimits.length - 1]);
+ String sourceSpaces = sourceText.substring(lastSourceWordEnd, sourceText.length());
+
+ int lastCommonTextWordEnd = previousCommonWordIndex < 0 ? 0 :
+ extractRangeEndFromLong(wordLimits[previousCommonWordIndex]);
+ String textSpaces = text.substring(lastCommonTextWordEnd, textView.length());
+
+ if (!sourceSpaces.equals(textSpaces) && textSpaces.length() > 0) {
+ ssb.setSpan(highlightSpan(nbHighlightSpans++),
+ shift + lastCommonTextWordEnd, shift + textView.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ // Final part, text after the current suggestion range.
+ ssb.append(mText.subSequence(spanEnd, unionEnd).toString());
+ textView.setText(ssb);
+ }
+
public void hide() {
+ if ((mText instanceof Editable) && mSuggestionRangeSpan != null) {
+ ((Editable) mText).removeSpan(mSuggestionRangeSpan);
+ }
mContainer.dismiss();
}
@@ -8346,11 +8533,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void onClick(View view) {
if (view instanceof TextView) {
TextView textView = (TextView) view;
- Long range = ((Long) view.getTag());
- if (range != NO_SUGGESTIONS) {
- final int spanStart = extractRangeStartFromLong(range);
- final int spanEnd = extractRangeEndFromLong(range);
- ((Editable) mText).replace(spanStart, spanEnd, textView.getText());
+ SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag();
+ final int spanStart = suggestionInfo.spanStart;
+ final int spanEnd = suggestionInfo.spanEnd;
+ if (spanStart != NO_SUGGESTIONS) {
+ final int suggestionStart = suggestionInfo.suggestionStart;
+ final int suggestionEnd = suggestionInfo.suggestionEnd;
+ final String suggestion = textView.getText().subSequence(
+ suggestionStart, suggestionEnd).toString();
+ ((Editable) mText).replace(spanStart, spanEnd, suggestion);
}
}
hide();