Loading core/java/android/widget/TextView.java +201 −10 Original line number Original line Diff line number Diff line Loading @@ -60,6 +60,7 @@ import android.text.Selection; import android.text.SpanWatcher; import android.text.SpanWatcher; import android.text.Spannable; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.Spanned; import android.text.SpannedString; import android.text.SpannedString; import android.text.StaticLayout; import android.text.StaticLayout; Loading @@ -80,10 +81,13 @@ import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; import android.text.method.TransformationMethod; import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; import android.text.style.ParagraphStyle; import android.text.style.SuggestionSpan; import android.text.style.SuggestionSpan; import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.text.style.UpdateAppearance; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.text.util.Linkify; import android.util.AttributeSet; import android.util.AttributeSet; Loading Loading @@ -127,6 +131,7 @@ import android.widget.RemoteViews.RemoteView; import java.io.IOException; import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.ref.WeakReference; import java.text.BreakIterator; import java.util.ArrayList; import java.util.ArrayList; /** /** Loading Loading @@ -314,6 +319,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; private int mTextEditSuggestionItemLayout; private int mTextEditSuggestionItemLayout; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionRangeSpan mSuggestionRangeSpan; private int mCursorDrawableRes; private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; private final Drawable[] mCursorDrawable = new Drawable[2]; Loading Loading @@ -8225,13 +8231,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); 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 class SuggestionsPopupWindow implements OnClickListener { private static final int MAX_NUMBER_SUGGESTIONS = 5; 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 PopupWindow mContainer; private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; private final int[] mSuggestionViewLayouts = new int[] { private final int[] mSuggestionViewLayouts = new int[] { mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; private WordIterator mWordIterator; private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0]; public SuggestionsPopupWindow() { public SuggestionsPopupWindow() { mContainer = new PopupWindow(TextView.this.mContext, null, mContainer = new PopupWindow(TextView.this.mContext, null, Loading @@ -8244,6 +8257,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 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) { private ViewGroup getViewGroup(boolean under) { final int viewIndex = under ? 0 : 1; final int viewIndex = under ? 0 : 1; ViewGroup viewGroup = mSuggestionViews[viewIndex]; ViewGroup viewGroup = mSuggestionViews[viewIndex]; Loading Loading @@ -8277,6 +8295,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener "Inflated TextEdit suggestion item is not a TextView: " + childView); "Inflated TextEdit suggestion item is not a TextView: " + childView); } } childView.setTag(new SuggestionInfo()); viewGroup.addView(childView); viewGroup.addView(childView); childView.setOnClickListener(this); childView.setOnClickListener(this); } } Loading @@ -8299,21 +8318,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mContainer.setContentView(viewGroup); mContainer.setContentView(viewGroup); int totalNbSuggestions = 0; int totalNbSuggestions = 0; int spanUnionStart = mText.length(); int spanUnionEnd = 0; for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; final int spanStart = spannable.getSpanStart(suggestionSpan); final int spanStart = spannable.getSpanStart(suggestionSpan); final int spanEnd = spannable.getSpanEnd(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(); String[] suggestions = suggestionSpan.getSuggestions(); int nbSuggestions = suggestions.length; int nbSuggestions = suggestions.length; for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions); TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions); textView.setText(suggestions[suggestionIndex]); textView.setText(suggestions[suggestionIndex]); textView.setTag(spanRange); SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); suggestionInfo.spanStart = spanStart; suggestionInfo.spanEnd = spanEnd; totalNbSuggestions++; totalNbSuggestions++; if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) { if (totalNbSuggestions > MAX_NUMBER_SUGGESTIONS) { // Also end outer for loop spanIndex = nbSpans; spanIndex = nbSpans; break; break; } } Loading @@ -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... // TODO Replace by final text, use a dedicated layout, add a fade out timer... TextView textView = (TextView) viewGroup.getChildAt(0); TextView textView = (TextView) viewGroup.getChildAt(0); textView.setText("No suggestions available"); textView.setText("No suggestions available"); textView.setTag(NO_SUGGESTIONS); SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); suggestionInfo.spanStart = NO_SUGGESTIONS; totalNbSuggestions++; 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++) { for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { Loading @@ -8338,7 +8374,158 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener positionAtCursor(); 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() { public void hide() { if ((mText instanceof Editable) && mSuggestionRangeSpan != null) { ((Editable) mText).removeSpan(mSuggestionRangeSpan); } mContainer.dismiss(); mContainer.dismiss(); } } Loading @@ -8346,11 +8533,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onClick(View view) { public void onClick(View view) { if (view instanceof TextView) { if (view instanceof TextView) { TextView textView = (TextView) view; TextView textView = (TextView) view; Long range = ((Long) view.getTag()); SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); if (range != NO_SUGGESTIONS) { final int spanStart = suggestionInfo.spanStart; final int spanStart = extractRangeStartFromLong(range); final int spanEnd = suggestionInfo.spanEnd; final int spanEnd = extractRangeEndFromLong(range); if (spanStart != NO_SUGGESTIONS) { ((Editable) mText).replace(spanStart, spanEnd, textView.getText()); 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(); hide(); Loading core/res/res/drawable-hdpi/text_edit_suggestions_bottom_window.9.png 0 → 100644 +929 B Loading image diff... core/res/res/drawable-hdpi/text_edit_suggestions_top_window.9.png 0 → 100644 +943 B Loading image diff... core/res/res/layout/text_edit_suggestion_item.xml +2 −2 Original line number Original line Diff line number Diff line Loading @@ -21,7 +21,7 @@ android:paddingRight="16dip" android:paddingRight="16dip" android:paddingTop="8dip" android:paddingTop="8dip" android:paddingBottom="8dip" android:paddingBottom="8dip" android:layout_gravity="center" android:layout_gravity="left|center_vertical" android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="@android:color/black" /> android:textColor="@android:color/dim_foreground_light" /> core/res/res/values/colors.xml +1 −0 Original line number Original line Diff line number Diff line Loading @@ -60,6 +60,7 @@ <color name="highlighted_text_light">#9983CC39</color> <color name="highlighted_text_light">#9983CC39</color> <color name="link_text_dark">#5c5cff</color> <color name="link_text_dark">#5c5cff</color> <color name="link_text_light">#0000ee</color> <color name="link_text_light">#0000ee</color> <color name="suggestion_highlight_text">#177bbd</color> <drawable name="stat_notify_sync_noanim">@drawable/stat_notify_sync_anim0</drawable> <drawable name="stat_notify_sync_noanim">@drawable/stat_notify_sync_anim0</drawable> <drawable name="stat_sys_download_done">@drawable/stat_sys_download_anim0</drawable> <drawable name="stat_sys_download_done">@drawable/stat_sys_download_anim0</drawable> Loading Loading
core/java/android/widget/TextView.java +201 −10 Original line number Original line Diff line number Diff line Loading @@ -60,6 +60,7 @@ import android.text.Selection; import android.text.SpanWatcher; import android.text.SpanWatcher; import android.text.Spannable; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.Spanned; import android.text.SpannedString; import android.text.SpannedString; import android.text.StaticLayout; import android.text.StaticLayout; Loading @@ -80,10 +81,13 @@ import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; import android.text.method.TransformationMethod; import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; import android.text.style.ParagraphStyle; import android.text.style.SuggestionSpan; import android.text.style.SuggestionSpan; import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.text.style.UpdateAppearance; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.text.util.Linkify; import android.util.AttributeSet; import android.util.AttributeSet; Loading Loading @@ -127,6 +131,7 @@ import android.widget.RemoteViews.RemoteView; import java.io.IOException; import java.io.IOException; import java.lang.ref.WeakReference; import java.lang.ref.WeakReference; import java.text.BreakIterator; import java.util.ArrayList; import java.util.ArrayList; /** /** Loading Loading @@ -314,6 +319,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; private int mTextEditSuggestionItemLayout; private int mTextEditSuggestionItemLayout; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionRangeSpan mSuggestionRangeSpan; private int mCursorDrawableRes; private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; private final Drawable[] mCursorDrawable = new Drawable[2]; Loading Loading @@ -8225,13 +8231,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); 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 class SuggestionsPopupWindow implements OnClickListener { private static final int MAX_NUMBER_SUGGESTIONS = 5; 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 PopupWindow mContainer; private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; private final int[] mSuggestionViewLayouts = new int[] { private final int[] mSuggestionViewLayouts = new int[] { mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; private WordIterator mWordIterator; private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0]; public SuggestionsPopupWindow() { public SuggestionsPopupWindow() { mContainer = new PopupWindow(TextView.this.mContext, null, mContainer = new PopupWindow(TextView.this.mContext, null, Loading @@ -8244,6 +8257,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 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) { private ViewGroup getViewGroup(boolean under) { final int viewIndex = under ? 0 : 1; final int viewIndex = under ? 0 : 1; ViewGroup viewGroup = mSuggestionViews[viewIndex]; ViewGroup viewGroup = mSuggestionViews[viewIndex]; Loading Loading @@ -8277,6 +8295,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener "Inflated TextEdit suggestion item is not a TextView: " + childView); "Inflated TextEdit suggestion item is not a TextView: " + childView); } } childView.setTag(new SuggestionInfo()); viewGroup.addView(childView); viewGroup.addView(childView); childView.setOnClickListener(this); childView.setOnClickListener(this); } } Loading @@ -8299,21 +8318,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mContainer.setContentView(viewGroup); mContainer.setContentView(viewGroup); int totalNbSuggestions = 0; int totalNbSuggestions = 0; int spanUnionStart = mText.length(); int spanUnionEnd = 0; for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; final int spanStart = spannable.getSpanStart(suggestionSpan); final int spanStart = spannable.getSpanStart(suggestionSpan); final int spanEnd = spannable.getSpanEnd(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(); String[] suggestions = suggestionSpan.getSuggestions(); int nbSuggestions = suggestions.length; int nbSuggestions = suggestions.length; for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions); TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions); textView.setText(suggestions[suggestionIndex]); textView.setText(suggestions[suggestionIndex]); textView.setTag(spanRange); SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); suggestionInfo.spanStart = spanStart; suggestionInfo.spanEnd = spanEnd; totalNbSuggestions++; totalNbSuggestions++; if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) { if (totalNbSuggestions > MAX_NUMBER_SUGGESTIONS) { // Also end outer for loop spanIndex = nbSpans; spanIndex = nbSpans; break; break; } } Loading @@ -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... // TODO Replace by final text, use a dedicated layout, add a fade out timer... TextView textView = (TextView) viewGroup.getChildAt(0); TextView textView = (TextView) viewGroup.getChildAt(0); textView.setText("No suggestions available"); textView.setText("No suggestions available"); textView.setTag(NO_SUGGESTIONS); SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); suggestionInfo.spanStart = NO_SUGGESTIONS; totalNbSuggestions++; 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++) { for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { Loading @@ -8338,7 +8374,158 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener positionAtCursor(); 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() { public void hide() { if ((mText instanceof Editable) && mSuggestionRangeSpan != null) { ((Editable) mText).removeSpan(mSuggestionRangeSpan); } mContainer.dismiss(); mContainer.dismiss(); } } Loading @@ -8346,11 +8533,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onClick(View view) { public void onClick(View view) { if (view instanceof TextView) { if (view instanceof TextView) { TextView textView = (TextView) view; TextView textView = (TextView) view; Long range = ((Long) view.getTag()); SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); if (range != NO_SUGGESTIONS) { final int spanStart = suggestionInfo.spanStart; final int spanStart = extractRangeStartFromLong(range); final int spanEnd = suggestionInfo.spanEnd; final int spanEnd = extractRangeEndFromLong(range); if (spanStart != NO_SUGGESTIONS) { ((Editable) mText).replace(spanStart, spanEnd, textView.getText()); 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(); hide(); Loading
core/res/res/drawable-hdpi/text_edit_suggestions_bottom_window.9.png 0 → 100644 +929 B Loading image diff...
core/res/res/drawable-hdpi/text_edit_suggestions_top_window.9.png 0 → 100644 +943 B Loading image diff...
core/res/res/layout/text_edit_suggestion_item.xml +2 −2 Original line number Original line Diff line number Diff line Loading @@ -21,7 +21,7 @@ android:paddingRight="16dip" android:paddingRight="16dip" android:paddingTop="8dip" android:paddingTop="8dip" android:paddingBottom="8dip" android:paddingBottom="8dip" android:layout_gravity="center" android:layout_gravity="left|center_vertical" android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="@android:color/black" /> android:textColor="@android:color/dim_foreground_light" />
core/res/res/values/colors.xml +1 −0 Original line number Original line Diff line number Diff line Loading @@ -60,6 +60,7 @@ <color name="highlighted_text_light">#9983CC39</color> <color name="highlighted_text_light">#9983CC39</color> <color name="link_text_dark">#5c5cff</color> <color name="link_text_dark">#5c5cff</color> <color name="link_text_light">#0000ee</color> <color name="link_text_light">#0000ee</color> <color name="suggestion_highlight_text">#177bbd</color> <drawable name="stat_notify_sync_noanim">@drawable/stat_notify_sync_anim0</drawable> <drawable name="stat_notify_sync_noanim">@drawable/stat_notify_sync_anim0</drawable> <drawable name="stat_sys_download_done">@drawable/stat_sys_download_anim0</drawable> <drawable name="stat_sys_download_done">@drawable/stat_sys_download_anim0</drawable> Loading