Merge "Add EditTextShortcutSpan to provide edit short cut in the text view."

This commit is contained in:
Gilles Debunne
2011-08-30 15:53:33 -07:00
committed by Android (Google) Code Review
11 changed files with 351 additions and 69 deletions

View File

@@ -20689,6 +20689,14 @@ package android.text.style {
field protected final int mVerticalAlignment;
}
public class EasyEditSpan implements android.text.ParcelableSpan {
ctor public EasyEditSpan();
ctor public EasyEditSpan(android.os.Parcel);
method public int describeContents();
method public int getSpanTypeId();
method public void writeToParcel(android.os.Parcel, int);
}
public class ForegroundColorSpan extends android.text.style.CharacterStyle implements android.text.ParcelableSpan android.text.style.UpdateAppearance {
ctor public ForegroundColorSpan(int);
ctor public ForegroundColorSpan(android.os.Parcel);

View File

@@ -723,7 +723,7 @@ class TextLine {
float ret = 0;
int contextLen = contextEnd - contextStart;
if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineColor !=0 || runIsRtl))) {
if (needWidth || (c != null && (wp.bgColor != 0 || wp.underlineCount != 0 || runIsRtl))) {
int flags = runIsRtl ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR;
if (mCharsValid) {
ret = wp.getTextRunAdvances(mChars, start, runLen,
@@ -753,21 +753,26 @@ class TextLine {
wp.setColor(previousColor);
}
if (wp.underlineColor != 0) {
if (wp.underlineCount != 0) {
// kStdUnderline_Offset = 1/9, defined in SkTextFormatParams.h
float middle = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
// kStdUnderline_Thickness = 1/18, defined in SkTextFormatParams.h
float halfHeight = wp.underlineThickness * (1.0f / 18.0f / 2.0f) * wp.getTextSize();
float underlineTop = y + wp.baselineShift + (1.0f / 9.0f) * wp.getTextSize();
int previousColor = wp.getColor();
Paint.Style previousStyle = wp.getStyle();
boolean previousAntiAlias = wp.isAntiAlias();
wp.setColor(wp.underlineColor);
wp.setStyle(Paint.Style.FILL);
c.drawRect(x, middle - halfHeight, x + ret, middle + halfHeight, wp);
wp.setAntiAlias(true);
for (int i = 0; i < wp.underlineCount; i++) {
wp.setColor(wp.underlineColors[i]);
c.drawRect(x, underlineTop, x + ret, underlineTop + wp.underlineThicknesses[i],
wp);
}
wp.setStyle(previousStyle);
wp.setColor(previousColor);
wp.setAntiAlias(previousAntiAlias);
}
drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,

View File

@@ -23,6 +23,9 @@ import android.graphics.Paint;
* data used during text measuring and drawing.
*/
public class TextPaint extends Paint {
private static final int DEFAULT_UNDERLINE_SIZE = 3;
// Special value 0 means no background paint
public int bgColor;
public int baselineShift;
@@ -33,12 +36,17 @@ public class TextPaint extends Paint {
* Special value 0 means no custom underline
* @hide
*/
public int underlineColor;
public int[] underlineColors;
/**
* Defined as a multiplier of the default underline thickness. Use 1.0f for default thickness.
* @hide
*/
public float underlineThickness;
public float[] underlineThicknesses;
/**
* The number of underlines currently stored in the array. If 0, no underline is drawn.
* @hide
*/
public int underlineCount;
public TextPaint() {
super();
@@ -64,24 +72,43 @@ public class TextPaint extends Paint {
linkColor = tp.linkColor;
drawableState = tp.drawableState;
density = tp.density;
underlineColor = tp.underlineColor;
underlineThickness = tp.underlineThickness;
underlineColors = tp.underlineColors;
underlineThicknesses = tp.underlineThicknesses;
underlineCount = tp.underlineCount;
}
/**
* Defines a custom underline for this Paint.
* @param color underline solid color
* @param thickness underline thickness, defined as a multiplier of the default underline
* thickness.
* @param thickness underline thickness
* @hide
*/
public void setUnderlineText(boolean isUnderlined, int color, float thickness) {
setUnderlineText(false);
if (isUnderlined) {
underlineColor = color;
underlineThickness = thickness;
public void setUnderlineText(int color, float thickness) {
if (color == 0) {
// No underline
return;
}
if (underlineCount == 0) {
underlineColors = new int[DEFAULT_UNDERLINE_SIZE];
underlineThicknesses = new float[DEFAULT_UNDERLINE_SIZE];
underlineColors[underlineCount] = color;
underlineThicknesses[underlineCount] = thickness;
underlineCount++;
} else {
underlineColor = 0;
if (underlineCount == underlineColors.length) {
int[] newColors = new int[underlineColors.length + DEFAULT_UNDERLINE_SIZE];
float[] newThickness = new float[underlineThicknesses.length
+ DEFAULT_UNDERLINE_SIZE];
System.arraycopy(underlineColors, 0, newColors, 0, underlineColors.length);
System.arraycopy(
underlineThicknesses, 0, newThickness, 0, underlineThicknesses.length);
underlineColors = newColors;
underlineThicknesses = newThickness;
}
underlineColors[underlineCount] = color;
underlineThicknesses[underlineCount] = thickness;
underlineCount++;
}
}
}

View File

@@ -25,6 +25,7 @@ import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.EasyEditSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.MetricAffectingSpan;
import android.text.style.QuoteSpan;
@@ -585,6 +586,8 @@ public class TextUtils {
public static final int SPELL_CHECK_SPAN = 20;
/** @hide */
public static final int SUGGESTION_RANGE_SPAN = 21;
/** @hide */
public static final int EASY_EDIT_SPAN = 22;
/**
* Flatten a CharSequence and whatever styles can be copied across processes
@@ -748,6 +751,10 @@ public class TextUtils {
readSpan(p, sp, new SuggestionRangeSpan());
break;
case EASY_EDIT_SPAN:
readSpan(p, sp, new EasyEditSpan(p));
break;
default:
throw new RuntimeException("bogus span encoding " + kind);
}

View File

@@ -0,0 +1,54 @@
/*
* 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.style;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextUtils;
import android.widget.TextView;
/**
* Provides an easy way to edit a portion of text.
* <p>
* The {@link TextView} uses this span to allow the user to delete a chuck of text in one click.
* the text. {@link TextView} removes this span as soon as the text is edited, or the cursor moves.
*/
public class EasyEditSpan implements ParcelableSpan {
public EasyEditSpan() {
// Empty
}
public EasyEditSpan(Parcel src) {
this();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// Empty
}
@Override
public int getSpanTypeId() {
return TextUtils.EASY_EDIT_SPAN;
}
}

View File

@@ -61,12 +61,6 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before";
public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode";
/**
* The default underline thickness as a percentage of the system's default underline thickness
* (i.e., 100 means the default thickness, and 200 is a double thickness).
*/
private static final int DEFAULT_UNDERLINE_PERCENTAGE = 100;
public static final int SUGGESTIONS_MAX_SIZE = 5;
/*
@@ -82,10 +76,8 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
private final String mNotificationTargetClassName;
private final int mHashCode;
private float mMisspelledUnderlineThickness;
private int mMisspelledUnderlineColor;
private float mEasyCorrectUnderlineThickness;
private int mEasyCorrectUnderlineColor;
private float mUnderlineThickness;
private int mUnderlineColor;
/*
* TODO: If switching IME is required, needs to add parameters for ids of InputMethodInfo
@@ -140,31 +132,26 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
}
private void initStyle(Context context) {
// Read the colors. We need to store the color and the underline thickness, as the span
// does not have access to the context when it is read from a parcel.
TypedArray typedArray;
int defStyle = 0;
if ((getFlags() & FLAG_MISSPELLED) != 0) {
defStyle = com.android.internal.R.attr.textAppearanceMisspelledSuggestion;
} else if ((getFlags() & FLAG_EASY_CORRECT) != 0) {
defStyle = com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion;
} else {
// No style is applied.
mUnderlineThickness = 0;
mUnderlineColor = 0;
return;
}
typedArray = context.obtainStyledAttributes(null,
TypedArray typedArray = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.SuggestionSpan,
com.android.internal.R.attr.textAppearanceEasyCorrectSuggestion, 0);
defStyle, 0);
mEasyCorrectUnderlineThickness = getThicknessPercentage(typedArray,
com.android.internal.R.styleable.SuggestionSpan_textUnderlineThicknessPercentage);
mEasyCorrectUnderlineColor = typedArray.getColor(
mUnderlineThickness = typedArray.getDimension(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineThickness, 0);
mUnderlineColor = typedArray.getColor(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
typedArray = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.SuggestionSpan,
com.android.internal.R.attr.textAppearanceMisspelledSuggestion, 0);
mMisspelledUnderlineThickness = getThicknessPercentage(typedArray,
com.android.internal.R.styleable.SuggestionSpan_textUnderlineThicknessPercentage);
mMisspelledUnderlineColor = typedArray.getColor(
com.android.internal.R.styleable.SuggestionSpan_textUnderlineColor, Color.BLACK);
}
private static float getThicknessPercentage(TypedArray typedArray, int index) {
int value = typedArray.getInteger(index, DEFAULT_UNDERLINE_PERCENTAGE);
return value / 100.0f;
}
public SuggestionSpan(Parcel src) {
@@ -173,10 +160,8 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
mLocaleString = src.readString();
mNotificationTargetClassName = src.readString();
mHashCode = src.readInt();
mEasyCorrectUnderlineColor = src.readInt();
mEasyCorrectUnderlineThickness = src.readFloat();
mMisspelledUnderlineColor = src.readInt();
mMisspelledUnderlineThickness = src.readFloat();
mUnderlineColor = src.readInt();
mUnderlineThickness = src.readFloat();
}
/**
@@ -226,10 +211,8 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
dest.writeString(mLocaleString);
dest.writeString(mNotificationTargetClassName);
dest.writeInt(mHashCode);
dest.writeInt(mEasyCorrectUnderlineColor);
dest.writeFloat(mEasyCorrectUnderlineThickness);
dest.writeInt(mMisspelledUnderlineColor);
dest.writeFloat(mMisspelledUnderlineThickness);
dest.writeInt(mUnderlineColor);
dest.writeFloat(mUnderlineThickness);
}
@Override
@@ -271,10 +254,6 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan {
@Override
public void updateDrawState(TextPaint tp) {
if ((getFlags() & FLAG_MISSPELLED) != 0) {
tp.setUnderlineText(true, mMisspelledUnderlineColor, mMisspelledUnderlineThickness);
} else if ((getFlags() & FLAG_EASY_CORRECT) != 0) {
tp.setUnderlineText(true, mEasyCorrectUnderlineColor, mEasyCorrectUnderlineThickness);
}
tp.setUnderlineText(mUnderlineColor, mUnderlineThickness);
}
}

View File

@@ -81,7 +81,9 @@ import android.text.method.TimeKeyListener;
import android.text.method.TransformationMethod;
import android.text.method.TransformationMethod2;
import android.text.method.WordIterator;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.EasyEditSpan;
import android.text.style.ParagraphStyle;
import android.text.style.SpellCheckSpan;
import android.text.style.SuggestionRangeSpan;
@@ -7705,10 +7707,148 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
/**
* Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
* pop-up should be displayed.
*/
private class EditTextShortcutController {
private EditTextShortcutPopupWindow mPopupWindow;
private EasyEditSpan mEditTextShortcutSpan;
private void hide() {
if (mEditTextShortcutSpan != null) {
mPopupWindow.hide();
if (mText instanceof Spannable) {
((Spannable) mText).removeSpan(mEditTextShortcutSpan);
}
mEditTextShortcutSpan = null;
}
}
/**
* Monitors the changes in the text.
*
* <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
* as the notifications are not sent when a spannable (with spans) is inserted.
*/
public void onTextChange(CharSequence buffer) {
if (mEditTextShortcutSpan != null) {
hide();
}
if (buffer instanceof Spanned) {
mEditTextShortcutSpan = getSpan((Spanned) buffer);
if (mEditTextShortcutSpan != null) {
if (mPopupWindow == null) {
mPopupWindow = new EditTextShortcutPopupWindow();
}
mPopupWindow.show(mEditTextShortcutSpan);
}
}
}
private EasyEditSpan getSpan(Spanned spanned) {
EasyEditSpan[] inputMethodSpans = spanned.getSpans(0, spanned.length(),
EasyEditSpan.class);
if (inputMethodSpans.length == 0) {
return null;
} else {
return inputMethodSpans[0];
}
}
}
/**
* Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
* by {@link EditTextShortcutController}.
*/
private class EditTextShortcutPopupWindow extends PinnedPopupWindow
implements OnClickListener {
private static final int POPUP_TEXT_LAYOUT =
com.android.internal.R.layout.text_edit_action_popup_text;
private TextView mDeleteTextView;
private EasyEditSpan mEditTextShortcutSpan;
@Override
protected void createPopupWindow() {
mPopupWindow = new PopupWindow(TextView.this.mContext, null,
com.android.internal.R.attr.textSelectHandleWindowStyle);
mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
mPopupWindow.setClippingEnabled(true);
}
@Override
protected void initContentView() {
mContentView.setOrientation(LinearLayout.HORIZONTAL);
mContentView.setBackgroundResource(
com.android.internal.R.drawable.text_edit_side_paste_window);
LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutParams wrapContent = new LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
mDeleteTextView.setLayoutParams(wrapContent);
mDeleteTextView.setText(com.android.internal.R.string.delete);
mDeleteTextView.setOnClickListener(this);
mContentView.addView(mDeleteTextView);
}
public void show(EasyEditSpan inputMethodSpan) {
mEditTextShortcutSpan = inputMethodSpan;
super.show();
}
@Override
public void onClick(View view) {
if (view == mDeleteTextView) {
deleteText();
}
}
private void deleteText() {
Editable editable = (Editable) mText;
int start = editable.getSpanStart(mEditTextShortcutSpan);
int end = editable.getSpanEnd(mEditTextShortcutSpan);
if (start >= 0 && end >= 0) {
editable.delete(start, end);
}
}
@Override
protected int getTextOffset() {
// Place the pop-up at the end of the span
Editable editable = (Editable) mText;
return editable.getSpanEnd(mEditTextShortcutSpan);
}
@Override
protected int getVerticalLocalPosition(int line) {
return mLayout.getLineBottom(line);
}
@Override
protected int clipVertically(int positionY) {
// As we display the pop-up below the span, no vertical clipping is required.
return positionY;
}
}
private class ChangeWatcher implements TextWatcher, SpanWatcher {
private CharSequence mBeforeText;
private EditTextShortcutController mEditTextShortcutController;
private ChangeWatcher() {
mEditTextShortcutController = new EditTextShortcutController();
}
public void beforeTextChanged(CharSequence buffer, int start,
int before, int after) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
@@ -7729,6 +7869,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
+ " before=" + before + " after=" + after + ": " + buffer);
TextView.this.handleTextChanged(buffer, start, before, after);
mEditTextShortcutController.onTextChange(buffer);
if (AccessibilityManager.getInstance(mContext).isEnabled() &&
(isFocused() || isSelected() && isShown())) {
sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
@@ -7763,6 +7905,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
+ " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, -1, e, -1);
}
private void hideControllers() {
mEditTextShortcutController.hide();
}
}
/**
@@ -9184,6 +9330,34 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
private static class SuggestionRangeSpan extends CharacterStyle {
private final int mTextColor;
private final int mBackgroundColor;
public SuggestionRangeSpan(Context context) {
TypedArray typedArray = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.SuggestionRangeSpan,
com.android.internal.R.attr.textAppearanceSuggestionRange, 0);
mTextColor = typedArray.getColor(
com.android.internal.R.styleable.SuggestionRangeSpan_textColor, 0);
mBackgroundColor = typedArray.getColor(
com.android.internal.R.styleable.SuggestionRangeSpan_colorBackground, 0);
}
@Override
public void updateDrawState(TextPaint tp) {
if (mTextColor != 0) {
tp.setColor(mTextColor);
}
if (mBackgroundColor != 0) {
tp.bgColor = mBackgroundColor;
}
}
}
private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnClickListener {
private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
private static final int NO_SUGGESTIONS = -1;
@@ -9368,7 +9542,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (totalNbSuggestions == 0) return false;
if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
if (mSuggestionRangeSpan == null) {
mSuggestionRangeSpan = new SuggestionRangeSpan(getContext());
}
((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (mSuggestionRangeSpan == null) mSuggestionRangeSpan =
new SuggestionRangeSpan(getContext());
((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -10695,6 +10877,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private void hideControllers() {
hideInsertionPointCursorController();
stopSelectionActionMode();
if (mChangeWatcher != null) {
mChangeWatcher.hideControllers();
}
}
/**

View File

@@ -158,11 +158,14 @@
<!-- The underline color and thickness for misspelled suggestion -->
<attr name="textAppearanceMisspelledSuggestion" format="reference" />
<!-- The text color and the background for suggestion range span. This span identifies the
portion of text the suggestions refer to). -->
<attr name="textAppearanceSuggestionRange" format="reference" />
<!-- The underline color -->
<attr name="textUnderlineColor" format="reference|color" />
<!-- The underline thickness, expressed as a percentage of the default underline thickness
(i.e., 100 means default thickness, and 200 means double thickness). -->
<attr name="textUnderlineThicknessPercentage" format="reference|integer" />
<!-- The underline thickness -->
<attr name="textUnderlineThickness" format="reference|dimension" />
<!-- EditText text foreground color. -->
<attr name="editTextColor" format="reference|color" />
@@ -3149,7 +3152,11 @@
</declare-styleable>
<declare-styleable name="SuggestionSpan">
<attr name="textUnderlineColor" />
<attr name="textUnderlineThicknessPercentage" />
<attr name="textUnderlineThickness" />
</declare-styleable>
<declare-styleable name="SuggestionRangeSpan">
<attr name="textColor" />
<attr name="colorBackground" />
</declare-styleable>
<!-- An <code>input-extras</code> is a container for extra data to supply to
an input method. Contains

View File

@@ -2472,6 +2472,9 @@
<!-- Item on EditText context menu. This action is used to replace the current word by other suggested words, suggested by the IME or the spell checker -->
<string name="replace">Replace\u2026</string>
<!-- Item on EditText pop-up window. This action is used to delete the text that the user recently added. [CHAR LIMIT=15] -->
<string name="delete">Delete</string>
<!-- Item on EditText context menu. This action is used to copy a URL from the edit field into the clipboard. -->
<string name="copyUrl">Copy URL</string>

View File

@@ -244,7 +244,7 @@ please see styles_device_defaults.xml.
</style>
<style name="TextAppearance.Suggestion">
<item name="android:textUnderlineThicknessPercentage">200</item>
<item name="android:textUnderlineThickness">2dip</item>
</style>
<style name="TextAppearance.EasyCorrectSuggestion" parent="TextAppearance.Suggestion">
@@ -255,6 +255,11 @@ please see styles_device_defaults.xml.
<item name="android:textUnderlineColor">@color/holo_red_light</item>
</style>
<style name="TextAppearance.SuggestionRange">
<item name="android:textColor">@color/white</item>
<item name="android:colorBackground">@color/holo_blue_dark</item>
</style>
<!-- Widget Styles -->
<style name="Widget">

View File

@@ -91,6 +91,7 @@ please see themes_device_defaults.xml.
<item name="textAppearanceEasyCorrectSuggestion">@android:style/TextAppearance.EasyCorrectSuggestion</item>
<item name="textAppearanceMisspelledSuggestion">@android:style/TextAppearance.MisspelledSuggestion</item>
<item name="textAppearanceSuggestionRange">@android:style/TextAppearance.SuggestionRange</item>
<item name="textAppearanceButton">@android:style/TextAppearance.Widget.Button</item>