summaryrefslogtreecommitdiffstats
path: root/core/java/android/widget/TextView.java
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/widget/TextView.java
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz
frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/widget/TextView.java')
-rw-r--r--core/java/android/widget/TextView.java6787
1 files changed, 6787 insertions, 0 deletions
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
new file mode 100644
index 0000000..080f3de
--- /dev/null
+++ b/core/java/android/widget/TextView.java
@@ -0,0 +1,6787 @@
+/*
+ * Copyright (C) 2006 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.widget;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.Message;
+import android.text.BoringLayout;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.GraphicsOperations;
+import android.text.ClipboardManager;
+import android.text.InputFilter;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.SpannableString;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.DateKeyListener;
+import android.text.method.DateTimeKeyListener;
+import android.text.method.DialerKeyListener;
+import android.text.method.DigitsKeyListener;
+import android.text.method.KeyListener;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.TimeKeyListener;
+
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.SingleLineTransformationMethod;
+import android.text.method.TextKeyListener;
+import android.text.method.TransformationMethod;
+import android.text.style.ParagraphStyle;
+import android.text.style.URLSpan;
+import android.text.style.UpdateAppearance;
+import android.text.util.Linkify;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.FloatMath;
+import android.util.TypedValue;
+import android.view.ContextMenu;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewRoot;
+import android.view.ViewTreeObserver;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.AnimationUtils;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.RemoteViews.RemoteView;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+import com.android.internal.util.FastMath;
+import com.android.internal.widget.EditableInputConnection;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Displays text to the user and optionally allows them to edit it. A TextView
+ * is a complete text editor, however the basic class is configured to not
+ * allow editing; see {@link EditText} for a subclass that configures the text
+ * view for editing.
+ *
+ * <p>
+ * <b>XML attributes</b>
+ * <p>
+ * See {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ *
+ * @attr ref android.R.styleable#TextView_text
+ * @attr ref android.R.styleable#TextView_bufferType
+ * @attr ref android.R.styleable#TextView_hint
+ * @attr ref android.R.styleable#TextView_textColor
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ * @attr ref android.R.styleable#TextView_textColorHint
+ * @attr ref android.R.styleable#TextView_textSize
+ * @attr ref android.R.styleable#TextView_textScaleX
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ * @attr ref android.R.styleable#TextView_maxLines
+ * @attr ref android.R.styleable#TextView_maxHeight
+ * @attr ref android.R.styleable#TextView_lines
+ * @attr ref android.R.styleable#TextView_height
+ * @attr ref android.R.styleable#TextView_minLines
+ * @attr ref android.R.styleable#TextView_minHeight
+ * @attr ref android.R.styleable#TextView_maxEms
+ * @attr ref android.R.styleable#TextView_maxWidth
+ * @attr ref android.R.styleable#TextView_ems
+ * @attr ref android.R.styleable#TextView_width
+ * @attr ref android.R.styleable#TextView_minEms
+ * @attr ref android.R.styleable#TextView_minWidth
+ * @attr ref android.R.styleable#TextView_gravity
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ * @attr ref android.R.styleable#TextView_selectAllOnFocus
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ * @attr ref android.R.styleable#TextView_maxLength
+ * @attr ref android.R.styleable#TextView_shadowColor
+ * @attr ref android.R.styleable#TextView_shadowDx
+ * @attr ref android.R.styleable#TextView_shadowDy
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ * @attr ref android.R.styleable#TextView_autoLink
+ * @attr ref android.R.styleable#TextView_linksClickable
+ * @attr ref android.R.styleable#TextView_numeric
+ * @attr ref android.R.styleable#TextView_digits
+ * @attr ref android.R.styleable#TextView_phoneNumber
+ * @attr ref android.R.styleable#TextView_inputMethod
+ * @attr ref android.R.styleable#TextView_capitalize
+ * @attr ref android.R.styleable#TextView_autoText
+ * @attr ref android.R.styleable#TextView_editable
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ */
+@RemoteView
+public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
+ static final String TAG = "TextView";
+ static final boolean DEBUG_EXTRACT = false;
+
+ private static int PRIORITY = 100;
+
+ private ColorStateList mTextColor;
+ private int mCurTextColor;
+ private ColorStateList mHintTextColor;
+ private ColorStateList mLinkTextColor;
+ private int mCurHintTextColor;
+ private boolean mFreezesText;
+ private boolean mFrozenWithFocus;
+ private boolean mTemporaryDetach;
+
+ private boolean mEatTouchRelease = false;
+ private boolean mScrolled = false;
+
+ private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
+ private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
+
+ private float mShadowRadius, mShadowDx, mShadowDy;
+
+ private static final int PREDRAW_NOT_REGISTERED = 0;
+ private static final int PREDRAW_PENDING = 1;
+ private static final int PREDRAW_DONE = 2;
+ private int mPreDrawState = PREDRAW_NOT_REGISTERED;
+
+ private TextUtils.TruncateAt mEllipsize = null;
+
+ // Enum for the "typeface" XML parameter.
+ // TODO: How can we get this from the XML instead of hardcoding it here?
+ private static final int SANS = 1;
+ private static final int SERIF = 2;
+ private static final int MONOSPACE = 3;
+
+ // Bitfield for the "numeric" XML parameter.
+ // TODO: How can we get this from the XML instead of hardcoding it here?
+ private static final int SIGNED = 2;
+ private static final int DECIMAL = 4;
+
+ class Drawables {
+ final Rect mCompoundRect = new Rect();
+ Drawable mDrawableTop, mDrawableBottom, mDrawableLeft, mDrawableRight;
+ int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight;
+ int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight;
+ int mDrawablePadding;
+ }
+ private Drawables mDrawables;
+
+ private CharSequence mError;
+ private boolean mErrorWasChanged;
+ private PopupWindow mPopup;
+ /**
+ * This flag is set if the TextView tries to display an error before it
+ * is attached to the window (so its position is still unknown).
+ * It causes the error to be shown later, when onAttachedToWindow()
+ * is called.
+ */
+ private boolean mShowErrorAfterAttach;
+
+ private CharWrapper mCharWrapper = null;
+
+ private boolean mSelectionMoved = false;
+
+ private Marquee mMarquee;
+ private boolean mRestartMarquee;
+
+ private int mMarqueeRepeatLimit = 3;
+
+ class InputContentType {
+ int imeOptions = EditorInfo.IME_UNDEFINED;
+ String privateImeOptions;
+ CharSequence imeActionLabel;
+ int imeActionId;
+ Bundle extras;
+ OnEditorActionListener onEditorActionListener;
+ boolean enterDown;
+ }
+ InputContentType mInputContentType;
+
+ class InputMethodState {
+ Rect mCursorRectInWindow = new Rect();
+ RectF mTmpRectF = new RectF();
+ float[] mTmpOffset = new float[2];
+ ExtractedTextRequest mExtracting;
+ final ExtractedText mTmpExtracted = new ExtractedText();
+ int mBatchEditNesting;
+ boolean mCursorChanged;
+ boolean mContentChanged;
+ int mChangedStart, mChangedEnd, mChangedDelta;
+ }
+ InputMethodState mInputMethodState;
+
+ /*
+ * Kick-start the font cache for the zygote process (to pay the cost of
+ * initializing freetype for our default font only once).
+ */
+ static {
+ Paint p = new Paint();
+ p.setAntiAlias(true);
+ // We don't care about the result, just the side-effect of measuring.
+ p.measureText("H");
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when an action is
+ * performed on the editor.
+ */
+ public interface OnEditorActionListener {
+ /**
+ * Called when an action is being performed.
+ *
+ * @param v The view that was clicked.
+ * @param actionId Identifier of the action. This will be either the
+ * identifier you supplied, or {@link EditorInfo#IME_UNDEFINED
+ * EditorInfo.IME_UNDEFINED} if being called due to the enter key
+ * being pressed.
+ * @param event If triggered by an enter key, this is the event;
+ * otherwise, this is null.
+ * @return Return true if you have consumed the action, else false.
+ */
+ boolean onEditorAction(TextView v, int actionId, KeyEvent event);
+ }
+
+ public TextView(Context context) {
+ this(context, null);
+ }
+
+ public TextView(Context context,
+ AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.textViewStyle);
+ }
+
+ public TextView(Context context,
+ AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ mText = "";
+
+ mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ // If we get the paint from the skin, we should set it to left, since
+ // the layout always wants it to be left.
+ // mTextPaint.setTextAlign(Paint.Align.LEFT);
+
+ mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ mMovement = getDefaultMovementMethod();
+ mTransformation = null;
+
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, com.android.internal.R.styleable.TextView, defStyle, 0);
+
+ int textColorHighlight = 0;
+ ColorStateList textColor = null;
+ ColorStateList textColorHint = null;
+ ColorStateList textColorLink = null;
+ int textSize = 15;
+ int typefaceIndex = -1;
+ int styleIndex = -1;
+
+ /*
+ * Look the appearance up without checking first if it exists because
+ * almost every TextView has one and it greatly simplifies the logic
+ * to be able to parse the appearance first and then let specific tags
+ * for this View override it.
+ */
+ TypedArray appearance = null;
+ int ap = a.getResourceId(com.android.internal.R.styleable.TextView_textAppearance, -1);
+ if (ap != -1) {
+ appearance = context.obtainStyledAttributes(ap,
+ com.android.internal.R.styleable.
+ TextAppearance);
+ }
+ if (appearance != null) {
+ int n = appearance.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = appearance.getIndex(i);
+
+ switch (attr) {
+ case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
+ textColorHighlight = appearance.getColor(attr, textColorHighlight);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textColor:
+ textColor = appearance.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textColorHint:
+ textColorHint = appearance.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textColorLink:
+ textColorLink = appearance.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textSize:
+ textSize = appearance.getDimensionPixelSize(attr, textSize);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_typeface:
+ typefaceIndex = appearance.getInt(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextAppearance_textStyle:
+ styleIndex = appearance.getInt(attr, -1);
+ break;
+ }
+ }
+
+ appearance.recycle();
+ }
+
+ boolean editable = getDefaultEditable();
+ CharSequence inputMethod = null;
+ int numeric = 0;
+ CharSequence digits = null;
+ boolean phone = false;
+ boolean autotext = false;
+ int autocap = -1;
+ int buffertype = 0;
+ boolean selectallonfocus = false;
+ Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
+ drawableBottom = null;
+ int drawablePadding = 0;
+ int ellipsize = -1;
+ boolean singleLine = false;
+ int maxlength = -1;
+ CharSequence text = "";
+ int shadowcolor = 0;
+ float dx = 0, dy = 0, r = 0;
+ boolean password = false;
+ int inputType = EditorInfo.TYPE_NULL;
+
+ int n = a.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ switch (attr) {
+ case com.android.internal.R.styleable.TextView_editable:
+ editable = a.getBoolean(attr, editable);
+ break;
+
+ case com.android.internal.R.styleable.TextView_inputMethod:
+ inputMethod = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_numeric:
+ numeric = a.getInt(attr, numeric);
+ break;
+
+ case com.android.internal.R.styleable.TextView_digits:
+ digits = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_phoneNumber:
+ phone = a.getBoolean(attr, phone);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoText:
+ autotext = a.getBoolean(attr, autotext);
+ break;
+
+ case com.android.internal.R.styleable.TextView_capitalize:
+ autocap = a.getInt(attr, autocap);
+ break;
+
+ case com.android.internal.R.styleable.TextView_bufferType:
+ buffertype = a.getInt(attr, buffertype);
+ break;
+
+ case com.android.internal.R.styleable.TextView_selectAllOnFocus:
+ selectallonfocus = a.getBoolean(attr, selectallonfocus);
+ break;
+
+ case com.android.internal.R.styleable.TextView_autoLink:
+ mAutoLinkMask = a.getInt(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_linksClickable:
+ mLinksClickable = a.getBoolean(attr, true);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableLeft:
+ drawableLeft = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableTop:
+ drawableTop = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableRight:
+ drawableRight = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawableBottom:
+ drawableBottom = a.getDrawable(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_drawablePadding:
+ drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxLines:
+ setMaxLines(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxHeight:
+ setMaxHeight(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_lines:
+ setLines(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_height:
+ setHeight(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minLines:
+ setMinLines(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minHeight:
+ setMinHeight(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxEms:
+ setMaxEms(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxWidth:
+ setMaxWidth(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_ems:
+ setEms(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_width:
+ setWidth(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minEms:
+ setMinEms(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_minWidth:
+ setMinWidth(a.getDimensionPixelSize(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_gravity:
+ setGravity(a.getInt(attr, -1));
+ break;
+
+ case com.android.internal.R.styleable.TextView_hint:
+ setHint(a.getText(attr));
+ break;
+
+ case com.android.internal.R.styleable.TextView_text:
+ text = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_scrollHorizontally:
+ if (a.getBoolean(attr, false)) {
+ setHorizontallyScrolling(true);
+ }
+ break;
+
+ case com.android.internal.R.styleable.TextView_singleLine:
+ singleLine = a.getBoolean(attr, singleLine);
+ break;
+
+ case com.android.internal.R.styleable.TextView_ellipsize:
+ ellipsize = a.getInt(attr, ellipsize);
+ break;
+
+ case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
+ setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
+ break;
+
+ case com.android.internal.R.styleable.TextView_includeFontPadding:
+ if (!a.getBoolean(attr, true)) {
+ setIncludeFontPadding(false);
+ }
+ break;
+
+ case com.android.internal.R.styleable.TextView_cursorVisible:
+ if (!a.getBoolean(attr, true)) {
+ setCursorVisible(false);
+ }
+ break;
+
+ case com.android.internal.R.styleable.TextView_maxLength:
+ maxlength = a.getInt(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textScaleX:
+ setTextScaleX(a.getFloat(attr, 1.0f));
+ break;
+
+ case com.android.internal.R.styleable.TextView_freezesText:
+ mFreezesText = a.getBoolean(attr, false);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowColor:
+ shadowcolor = a.getInt(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowDx:
+ dx = a.getFloat(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowDy:
+ dy = a.getFloat(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_shadowRadius:
+ r = a.getFloat(attr, 0);
+ break;
+
+ case com.android.internal.R.styleable.TextView_enabled:
+ setEnabled(a.getBoolean(attr, isEnabled()));
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColorHighlight:
+ textColorHighlight = a.getColor(attr, textColorHighlight);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColor:
+ textColor = a.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColorHint:
+ textColorHint = a.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textColorLink:
+ textColorLink = a.getColorStateList(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textSize:
+ textSize = a.getDimensionPixelSize(attr, textSize);
+ break;
+
+ case com.android.internal.R.styleable.TextView_typeface:
+ typefaceIndex = a.getInt(attr, typefaceIndex);
+ break;
+
+ case com.android.internal.R.styleable.TextView_textStyle:
+ styleIndex = a.getInt(attr, styleIndex);
+ break;
+
+ case com.android.internal.R.styleable.TextView_password:
+ password = a.getBoolean(attr, password);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lineSpacingExtra:
+ mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
+ mSpacingMult = a.getFloat(attr, mSpacingMult);
+ break;
+
+ case com.android.internal.R.styleable.TextView_inputType:
+ inputType = a.getInt(attr, mInputType);
+ break;
+
+ case com.android.internal.R.styleable.TextView_imeOptions:
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ mInputContentType.imeOptions = a.getInt(attr,
+ mInputContentType.imeOptions);
+ break;
+
+ case com.android.internal.R.styleable.TextView_imeActionLabel:
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ mInputContentType.imeActionLabel = a.getText(attr);
+ break;
+
+ case com.android.internal.R.styleable.TextView_imeActionId:
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ mInputContentType.imeActionId = a.getInt(attr,
+ mInputContentType.imeActionId);
+ break;
+
+ case com.android.internal.R.styleable.TextView_privateImeOptions:
+ setPrivateImeOptions(a.getString(attr));
+ break;
+
+ case com.android.internal.R.styleable.TextView_editorExtras:
+ try {
+ setInputExtras(a.getResourceId(attr, 0));
+ } catch (XmlPullParserException e) {
+ Log.w("TextView", "Failure reading input extras", e);
+ } catch (IOException e) {
+ Log.w("TextView", "Failure reading input extras", e);
+ }
+ break;
+ }
+ }
+ a.recycle();
+
+ BufferType bufferType = BufferType.EDITABLE;
+
+ if ((inputType&(EditorInfo.TYPE_MASK_CLASS
+ |EditorInfo.TYPE_MASK_VARIATION))
+ == (EditorInfo.TYPE_CLASS_TEXT
+ |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) {
+ password = true;
+ }
+
+ if (inputMethod != null) {
+ Class c;
+
+ try {
+ c = Class.forName(inputMethod.toString());
+ } catch (ClassNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ try {
+ mInput = (KeyListener) c.newInstance();
+ } catch (InstantiationException ex) {
+ throw new RuntimeException(ex);
+ } catch (IllegalAccessException ex) {
+ throw new RuntimeException(ex);
+ }
+ try {
+ mInputType = inputType != EditorInfo.TYPE_NULL
+ ? inputType
+ : mInput.getInputType();
+ } catch (IncompatibleClassChangeError e) {
+ mInputType = EditorInfo.TYPE_CLASS_TEXT;
+ }
+ } else if (digits != null) {
+ mInput = DigitsKeyListener.getInstance(digits.toString());
+ mInputType = inputType;
+ } else if (inputType != EditorInfo.TYPE_NULL) {
+ setInputType(inputType, true);
+ singleLine = (inputType&(EditorInfo.TYPE_MASK_CLASS
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) !=
+ (EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
+ } else if (phone) {
+ mInput = DialerKeyListener.getInstance();
+ inputType = EditorInfo.TYPE_CLASS_PHONE;
+ } else if (numeric != 0) {
+ mInput = DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
+ (numeric & DECIMAL) != 0);
+ inputType = EditorInfo.TYPE_CLASS_NUMBER;
+ if ((numeric & SIGNED) != 0) {
+ inputType |= EditorInfo.TYPE_NUMBER_FLAG_SIGNED;
+ }
+ if ((numeric & DECIMAL) != 0) {
+ inputType |= EditorInfo.TYPE_NUMBER_FLAG_DECIMAL;
+ }
+ mInputType = inputType;
+ } else if (autotext || autocap != -1) {
+ TextKeyListener.Capitalize cap;
+
+ inputType = EditorInfo.TYPE_CLASS_TEXT;
+ if (!singleLine) {
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+
+ switch (autocap) {
+ case 1:
+ cap = TextKeyListener.Capitalize.SENTENCES;
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
+ break;
+
+ case 2:
+ cap = TextKeyListener.Capitalize.WORDS;
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+ break;
+
+ case 3:
+ cap = TextKeyListener.Capitalize.CHARACTERS;
+ inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+ break;
+
+ default:
+ cap = TextKeyListener.Capitalize.NONE;
+ break;
+ }
+
+ mInput = TextKeyListener.getInstance(autotext, cap);
+ mInputType = inputType;
+ } else if (editable) {
+ mInput = TextKeyListener.getInstance();
+ mInputType = EditorInfo.TYPE_CLASS_TEXT;
+ } else {
+ mInput = null;
+
+ switch (buffertype) {
+ case 0:
+ bufferType = BufferType.NORMAL;
+ break;
+ case 1:
+ bufferType = BufferType.SPANNABLE;
+ break;
+ case 2:
+ bufferType = BufferType.EDITABLE;
+ break;
+ }
+ }
+
+ if (password && (mInputType&EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_TEXT) {
+ mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+ | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+ }
+
+ if (selectallonfocus) {
+ mSelectAllOnFocus = true;
+
+ if (bufferType == BufferType.NORMAL)
+ bufferType = BufferType.SPANNABLE;
+ }
+
+ setCompoundDrawablesWithIntrinsicBounds(
+ drawableLeft, drawableTop, drawableRight, drawableBottom);
+ setCompoundDrawablePadding(drawablePadding);
+
+ if (singleLine) {
+ setSingleLine();
+
+ if (mInput == null && ellipsize < 0) {
+ ellipsize = 3; // END
+ }
+ }
+
+ switch (ellipsize) {
+ case 1:
+ setEllipsize(TextUtils.TruncateAt.START);
+ break;
+ case 2:
+ setEllipsize(TextUtils.TruncateAt.MIDDLE);
+ break;
+ case 3:
+ setEllipsize(TextUtils.TruncateAt.END);
+ break;
+ case 4:
+ setHorizontalFadingEdgeEnabled(true);
+ setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ break;
+ }
+
+ setTextColor(textColor != null ? textColor : ColorStateList.valueOf(0xFF000000));
+ setHintTextColor(textColorHint);
+ setLinkTextColor(textColorLink);
+ if (textColorHighlight != 0) {
+ setHighlightColor(textColorHighlight);
+ }
+ setRawTextSize(textSize);
+
+ if (password) {
+ setTransformationMethod(PasswordTransformationMethod.getInstance());
+ typefaceIndex = MONOSPACE;
+ }
+
+ setTypefaceByIndex(typefaceIndex, styleIndex);
+
+ if (shadowcolor != 0) {
+ setShadowLayer(r, dx, dy, shadowcolor);
+ }
+
+ if (maxlength >= 0) {
+ setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
+ } else {
+ setFilters(NO_FILTERS);
+ }
+
+ setText(text, bufferType);
+
+ /*
+ * Views are not normally focusable unless specified to be.
+ * However, TextViews that have input or movement methods *are*
+ * focusable by default.
+ */
+ a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.View,
+ defStyle, 0);
+
+ boolean focusable = mMovement != null || mInput != null;
+ boolean clickable = focusable;
+ boolean longClickable = focusable;
+
+ n = a.getIndexCount();
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ switch (attr) {
+ case com.android.internal.R.styleable.View_focusable:
+ focusable = a.getBoolean(attr, focusable);
+ break;
+
+ case com.android.internal.R.styleable.View_clickable:
+ clickable = a.getBoolean(attr, clickable);
+ break;
+
+ case com.android.internal.R.styleable.View_longClickable:
+ longClickable = a.getBoolean(attr, longClickable);
+ break;
+ }
+ }
+ a.recycle();
+
+ setFocusable(focusable);
+ setClickable(clickable);
+ setLongClickable(longClickable);
+ }
+
+ private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
+ Typeface tf = null;
+ switch (typefaceIndex) {
+ case SANS:
+ tf = Typeface.SANS_SERIF;
+ break;
+
+ case SERIF:
+ tf = Typeface.SERIF;
+ break;
+
+ case MONOSPACE:
+ tf = Typeface.MONOSPACE;
+ break;
+ }
+
+ setTypeface(tf, styleIndex);
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed,
+ * and turns on the fake bold and italic bits in the Paint if the
+ * Typeface that you provided does not have all the bits in the
+ * style that you specified.
+ *
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public void setTypeface(Typeface tf, int style) {
+ if (style > 0) {
+ if (tf == null) {
+ tf = Typeface.defaultFromStyle(style);
+ } else {
+ tf = Typeface.create(tf, style);
+ }
+
+ setTypeface(tf);
+ // now compute what (if any) algorithmic styling is needed
+ int typefaceStyle = tf != null ? tf.getStyle() : 0;
+ int need = style & ~typefaceStyle;
+ mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
+ mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
+ } else {
+ mTextPaint.setFakeBoldText(false);
+ mTextPaint.setTextSkewX(0);
+ setTypeface(tf);
+ }
+ }
+
+ /**
+ * Subclasses override this to specify that they have a KeyListener
+ * by default even if not specifically called for in the XML options.
+ */
+ protected boolean getDefaultEditable() {
+ return false;
+ }
+
+ /**
+ * Subclasses override this to specify a default movement method.
+ */
+ protected MovementMethod getDefaultMovementMethod() {
+ return null;
+ }
+
+ /**
+ * Return the text the TextView is displaying. If setText() was called with
+ * an argument of BufferType.SPANNABLE or BufferType.EDITABLE, you can cast
+ * the return value from this method to Spannable or Editable, respectively.
+ *
+ * Note: The content of the return value should not be modified. If you want
+ * a modifiable one, you should make your own copy first.
+ */
+ @ViewDebug.CapturedViewProperty
+ public CharSequence getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the length, in characters, of the text managed by this TextView
+ */
+ public int length() {
+ return mText.length();
+ }
+
+ /**
+ * Return the text the TextView is displaying as an Editable object. If
+ * the text is not editable, null is returned.
+ *
+ * @see #getText
+ */
+ public Editable getEditableText() {
+ return (mText instanceof Editable) ? (Editable)mText : null;
+ }
+
+ /**
+ * @return the height of one standard line in pixels. Note that markup
+ * within the text can cause individual lines to be taller or shorter
+ * than this height, and the layout may contain additional first-
+ * or last-line padding.
+ */
+ public int getLineHeight() {
+ return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult
+ + mSpacingAdd);
+ }
+
+ /**
+ * @return the Layout that is currently being used to display the text.
+ * This can be null if the text or width has recently changes.
+ */
+ public final Layout getLayout() {
+ return mLayout;
+ }
+
+ /**
+ * @return the current key listener for this TextView.
+ * This will frequently be null for non-EditText TextViews.
+ */
+ public final KeyListener getKeyListener() {
+ return mInput;
+ }
+
+ /**
+ * Sets the key listener to be used with this TextView. This can be null
+ * to disallow user input. Note that this method has significant and
+ * subtle interactions with soft keyboards and other input method:
+ * see {@link KeyListener#getInputType() KeyListener.getContentType()}
+ * for important details. Calling this method will replace the current
+ * content type of the text view with the content type returned by the
+ * key listener.
+ * <p>
+ * Be warned that if you want a TextView with a key listener or movement
+ * method not to be focusable, or if you want a TextView without a
+ * key listener or movement method to be focusable, you must call
+ * {@link #setFocusable} again after calling this to get the focusability
+ * back the way you want it.
+ *
+ * @attr ref android.R.styleable#TextView_numeric
+ * @attr ref android.R.styleable#TextView_digits
+ * @attr ref android.R.styleable#TextView_phoneNumber
+ * @attr ref android.R.styleable#TextView_inputMethod
+ * @attr ref android.R.styleable#TextView_capitalize
+ * @attr ref android.R.styleable#TextView_autoText
+ */
+ public void setKeyListener(KeyListener input) {
+ setKeyListenerOnly(input);
+ fixFocusableAndClickableSettings();
+
+ if (input != null) {
+ try {
+ mInputType = mInput.getInputType();
+ } catch (IncompatibleClassChangeError e) {
+ mInputType = EditorInfo.TYPE_CLASS_TEXT;
+ }
+ if ((mInputType&EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_TEXT) {
+ if (mSingleLine) {
+ mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ } else {
+ mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ }
+ } else {
+ mInputType = EditorInfo.TYPE_NULL;
+ }
+
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) imm.restartInput(this);
+ }
+
+ private void setKeyListenerOnly(KeyListener input) {
+ mInput = input;
+ if (mInput != null && !(mText instanceof Editable))
+ setText(mText);
+
+ setFilters((Editable) mText, mFilters);
+ }
+
+ /**
+ * @return the movement method being used for this TextView.
+ * This will frequently be null for non-EditText TextViews.
+ */
+ public final MovementMethod getMovementMethod() {
+ return mMovement;
+ }
+
+ /**
+ * Sets the movement method (arrow key handler) to be used for
+ * this TextView. This can be null to disallow using the arrow keys
+ * to move the cursor or scroll the view.
+ * <p>
+ * Be warned that if you want a TextView with a key listener or movement
+ * method not to be focusable, or if you want a TextView without a
+ * key listener or movement method to be focusable, you must call
+ * {@link #setFocusable} again after calling this to get the focusability
+ * back the way you want it.
+ */
+ public final void setMovementMethod(MovementMethod movement) {
+ mMovement = movement;
+
+ if (mMovement != null && !(mText instanceof Spannable))
+ setText(mText);
+
+ fixFocusableAndClickableSettings();
+ }
+
+ private void fixFocusableAndClickableSettings() {
+ if ((mMovement != null) || mInput != null) {
+ setFocusable(true);
+ setClickable(true);
+ setLongClickable(true);
+ } else {
+ setFocusable(false);
+ setClickable(false);
+ setLongClickable(false);
+ }
+ }
+
+ /**
+ * @return the current transformation method for this TextView.
+ * This will frequently be null except for single-line and password
+ * fields.
+ */
+ public final TransformationMethod getTransformationMethod() {
+ return mTransformation;
+ }
+
+ /**
+ * Sets the transformation that is applied to the text that this
+ * TextView is displaying.
+ *
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public final void setTransformationMethod(TransformationMethod method) {
+ if (method == mTransformation) {
+ // Avoid the setText() below if the transformation is
+ // the same.
+ return;
+ }
+ if (mTransformation != null) {
+ if (mText instanceof Spannable) {
+ ((Spannable) mText).removeSpan(mTransformation);
+ }
+ }
+
+ mTransformation = method;
+
+ setText(mText);
+ }
+
+ /**
+ * Returns the top padding of the view, plus space for the top
+ * Drawable if any.
+ */
+ public int getCompoundPaddingTop() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mDrawableTop == null) {
+ return mPaddingTop;
+ } else {
+ return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
+ }
+ }
+
+ /**
+ * Returns the bottom padding of the view, plus space for the bottom
+ * Drawable if any.
+ */
+ public int getCompoundPaddingBottom() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mDrawableBottom == null) {
+ return mPaddingBottom;
+ } else {
+ return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
+ }
+ }
+
+ /**
+ * Returns the left padding of the view, plus space for the left
+ * Drawable if any.
+ */
+ public int getCompoundPaddingLeft() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mDrawableLeft == null) {
+ return mPaddingLeft;
+ } else {
+ return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
+ }
+ }
+
+ /**
+ * Returns the right padding of the view, plus space for the right
+ * Drawable if any.
+ */
+ public int getCompoundPaddingRight() {
+ final Drawables dr = mDrawables;
+ if (dr == null || dr.mDrawableRight == null) {
+ return mPaddingRight;
+ } else {
+ return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
+ }
+ }
+
+ /**
+ * Returns the extended top padding of the view, including both the
+ * top Drawable if any and any extra space to keep more than maxLines
+ * of text from showing. It is only valid to call this after measuring.
+ */
+ public int getExtendedPaddingTop() {
+ if (mMaxMode != LINES) {
+ return getCompoundPaddingTop();
+ }
+
+ if (mLayout.getLineCount() <= mMaximum) {
+ return getCompoundPaddingTop();
+ }
+
+ int top = getCompoundPaddingTop();
+ int bottom = getCompoundPaddingBottom();
+ int viewht = getHeight() - top - bottom;
+ int layoutht = mLayout.getLineTop(mMaximum);
+
+ if (layoutht >= viewht) {
+ return top;
+ }
+
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if (gravity == Gravity.TOP) {
+ return top;
+ } else if (gravity == Gravity.BOTTOM) {
+ return top + viewht - layoutht;
+ } else { // (gravity == Gravity.CENTER_VERTICAL)
+ return top + (viewht - layoutht) / 2;
+ }
+ }
+
+ /**
+ * Returns the extended bottom padding of the view, including both the
+ * bottom Drawable if any and any extra space to keep more than maxLines
+ * of text from showing. It is only valid to call this after measuring.
+ */
+ public int getExtendedPaddingBottom() {
+ if (mMaxMode != LINES) {
+ return getCompoundPaddingBottom();
+ }
+
+ if (mLayout.getLineCount() <= mMaximum) {
+ return getCompoundPaddingBottom();
+ }
+
+ int top = getCompoundPaddingTop();
+ int bottom = getCompoundPaddingBottom();
+ int viewht = getHeight() - top - bottom;
+ int layoutht = mLayout.getLineTop(mMaximum);
+
+ if (layoutht >= viewht) {
+ return bottom;
+ }
+
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ if (gravity == Gravity.TOP) {
+ return bottom + viewht - layoutht;
+ } else if (gravity == Gravity.BOTTOM) {
+ return bottom;
+ } else { // (gravity == Gravity.CENTER_VERTICAL)
+ return bottom + (viewht - layoutht) / 2;
+ }
+ }
+
+ /**
+ * Returns the total left padding of the view, including the left
+ * Drawable if any.
+ */
+ public int getTotalPaddingLeft() {
+ return getCompoundPaddingLeft();
+ }
+
+ /**
+ * Returns the total right padding of the view, including the right
+ * Drawable if any.
+ */
+ public int getTotalPaddingRight() {
+ return getCompoundPaddingRight();
+ }
+
+ /**
+ * Returns the total top padding of the view, including the top
+ * Drawable if any, the extra space to keep more than maxLines
+ * from showing, and the vertical offset for gravity, if any.
+ */
+ public int getTotalPaddingTop() {
+ return getExtendedPaddingTop() + getVerticalOffset(true);
+ }
+
+ /**
+ * Returns the total bottom padding of the view, including the bottom
+ * Drawable if any, the extra space to keep more than maxLines
+ * from showing, and the vertical offset for gravity, if any.
+ */
+ public int getTotalPaddingBottom() {
+ return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above,
+ * to the right of, and below the text. Use null if you do not
+ * want a Drawable there. The Drawables must already have had
+ * {@link Drawable#setBounds} called.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawables(Drawable left, Drawable top,
+ Drawable right, Drawable bottom) {
+ Drawables dr = mDrawables;
+
+ final boolean drawables = left != null || top != null
+ || right != null || bottom != null;
+
+ if (!drawables) {
+ // Clearing drawables... can we free the data structure?
+ if (dr != null) {
+ if (dr.mDrawablePadding == 0) {
+ mDrawables = null;
+ } else {
+ // We need to retain the last set padding, so just clear
+ // out all of the fields in the existing structure.
+ dr.mDrawableLeft = null;
+ dr.mDrawableTop = null;
+ dr.mDrawableRight = null;
+ dr.mDrawableBottom = null;
+ dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+ dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+ dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+ dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+ }
+ }
+ } else {
+ if (dr == null) {
+ mDrawables = dr = new Drawables();
+ }
+
+ dr.mDrawableLeft = left;
+ dr.mDrawableTop = top;
+ dr.mDrawableRight = right;
+ dr.mDrawableBottom = bottom;
+
+ final Rect compoundRect = dr.mCompoundRect;
+ int[] state = null;
+
+ state = getDrawableState();
+
+ if (left != null) {
+ left.setState(state);
+ left.copyBounds(compoundRect);
+ dr.mDrawableSizeLeft = compoundRect.width();
+ dr.mDrawableHeightLeft = compoundRect.height();
+ } else {
+ dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+ }
+
+ if (right != null) {
+ right.setState(state);
+ right.copyBounds(compoundRect);
+ dr.mDrawableSizeRight = compoundRect.width();
+ dr.mDrawableHeightRight = compoundRect.height();
+ } else {
+ dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+ }
+
+ if (top != null) {
+ top.setState(state);
+ top.copyBounds(compoundRect);
+ dr.mDrawableSizeTop = compoundRect.height();
+ dr.mDrawableWidthTop = compoundRect.width();
+ } else {
+ dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+ }
+
+ if (bottom != null) {
+ bottom.setState(state);
+ bottom.copyBounds(compoundRect);
+ dr.mDrawableSizeBottom = compoundRect.height();
+ dr.mDrawableWidthBottom = compoundRect.width();
+ } else {
+ dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+ }
+ }
+
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above,
+ * to the right of, and below the text. Use 0 if you do not
+ * want a Drawable there. The Drawables' bounds will be set to
+ * their intrinsic bounds.
+ *
+ * @param left Resource identifier of the left Drawable.
+ * @param top Resource identifier of the top Drawable.
+ * @param right Resource identifier of the right Drawable.
+ * @param bottom Resource identifier of the bottom Drawable.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
+ final Resources resources = getContext().getResources();
+ setCompoundDrawablesWithIntrinsicBounds(left != 0 ? resources.getDrawable(left) : null,
+ top != 0 ? resources.getDrawable(top) : null,
+ right != 0 ? resources.getDrawable(right) : null,
+ bottom != 0 ? resources.getDrawable(bottom) : null);
+ }
+
+ /**
+ * Sets the Drawables (if any) to appear to the left of, above,
+ * to the right of, and below the text. Use null if you do not
+ * want a Drawable there. The Drawables' bounds will be set to
+ * their intrinsic bounds.
+ *
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ */
+ public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
+ Drawable right, Drawable bottom) {
+
+ if (left != null) {
+ left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
+ }
+ if (right != null) {
+ right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
+ }
+ if (top != null) {
+ top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
+ }
+ if (bottom != null) {
+ bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
+ }
+ setCompoundDrawables(left, top, right, bottom);
+ }
+
+ /**
+ * Returns drawables for the left, top, right, and bottom borders.
+ */
+ public Drawable[] getCompoundDrawables() {
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ return new Drawable[] {
+ dr.mDrawableLeft, dr.mDrawableTop, dr.mDrawableRight, dr.mDrawableBottom
+ };
+ } else {
+ return new Drawable[] { null, null, null, null };
+ }
+ }
+
+ /**
+ * Sets the size of the padding between the compound drawables and
+ * the text.
+ *
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ */
+ public void setCompoundDrawablePadding(int pad) {
+ Drawables dr = mDrawables;
+ if (pad == 0) {
+ if (dr != null) {
+ dr.mDrawablePadding = pad;
+ }
+ } else {
+ if (dr == null) {
+ mDrawables = dr = new Drawables();
+ }
+ dr.mDrawablePadding = pad;
+ }
+
+ invalidate();
+ requestLayout();
+ }
+
+ /**
+ * Returns the padding between the compound drawables and the text.
+ */
+ public int getCompoundDrawablePadding() {
+ final Drawables dr = mDrawables;
+ return dr != null ? dr.mDrawablePadding : 0;
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ if (left != getPaddingLeft() ||
+ right != getPaddingRight() ||
+ top != getPaddingTop() ||
+ bottom != getPaddingBottom()) {
+ nullLayouts();
+ }
+
+ // the super call will requestLayout()
+ super.setPadding(left, top, right, bottom);
+ invalidate();
+ }
+
+ /**
+ * Gets the autolink mask of the text. See {@link
+ * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+ * possible values.
+ *
+ * @attr ref android.R.styleable#TextView_autoLink
+ */
+ public final int getAutoLinkMask() {
+ return mAutoLinkMask;
+ }
+
+ /**
+ * Sets the text color, size, style, hint color, and highlight color
+ * from the specified TextAppearance resource.
+ */
+ public void setTextAppearance(Context context, int resid) {
+ TypedArray appearance =
+ context.obtainStyledAttributes(resid,
+ com.android.internal.R.styleable.TextAppearance);
+
+ int color;
+ ColorStateList colors;
+ int ts;
+
+ color = appearance.getColor(com.android.internal.R.styleable.TextAppearance_textColorHighlight, 0);
+ if (color != 0) {
+ setHighlightColor(color);
+ }
+
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ if (colors != null) {
+ setTextColor(colors);
+ }
+
+ ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
+ TextAppearance_textSize, 0);
+ if (ts != 0) {
+ setRawTextSize(ts);
+ }
+
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColorHint);
+ if (colors != null) {
+ setHintTextColor(colors);
+ }
+
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColorLink);
+ if (colors != null) {
+ setLinkTextColor(colors);
+ }
+
+ int typefaceIndex, styleIndex;
+
+ typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
+ TextAppearance_typeface, -1);
+ styleIndex = appearance.getInt(com.android.internal.R.styleable.
+ TextAppearance_textStyle, -1);
+
+ setTypefaceByIndex(typefaceIndex, styleIndex);
+ appearance.recycle();
+ }
+
+ /**
+ * @return the size (in pixels) of the default text size in this TextView.
+ */
+ public float getTextSize() {
+ return mTextPaint.getTextSize();
+ }
+
+ /**
+ * Set the default text size to the given value, interpreted as "scaled
+ * pixel" units. This size is adjusted based on the current density and
+ * user font size preference.
+ *
+ * @param size The scaled pixel size.
+ *
+ * @attr ref android.R.styleable#TextView_textSize
+ */
+ @android.view.RemotableViewMethod
+ public void setTextSize(float size) {
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
+ }
+
+ /**
+ * Set the default text size to a given unit and value. See {@link
+ * TypedValue} for the possible dimension units.
+ *
+ * @param unit The desired dimension unit.
+ * @param size The desired size in the given units.
+ *
+ * @attr ref android.R.styleable#TextView_textSize
+ */
+ public void setTextSize(int unit, float size) {
+ Context c = getContext();
+ Resources r;
+
+ if (c == null)
+ r = Resources.getSystem();
+ else
+ r = c.getResources();
+
+ setRawTextSize(TypedValue.applyDimension(
+ unit, size, r.getDisplayMetrics()));
+ }
+
+ private void setRawTextSize(float size) {
+ if (size != mTextPaint.getTextSize()) {
+ mTextPaint.setTextSize(size);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * @return the extent by which text is currently being stretched
+ * horizontally. This will usually be 1.
+ */
+ public float getTextScaleX() {
+ return mTextPaint.getTextScaleX();
+ }
+
+ /**
+ * Sets the extent by which text should be stretched horizontally.
+ *
+ * @attr ref android.R.styleable#TextView_textScaleX
+ */
+ @android.view.RemotableViewMethod
+ public void setTextScaleX(float size) {
+ if (size != mTextPaint.getTextScaleX()) {
+ mTextPaint.setTextScaleX(size);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Sets the typeface and style in which the text should be displayed.
+ * Note that not all Typeface families actually have bold and italic
+ * variants, so you may need to use
+ * {@link #setTypeface(Typeface, int)} to get the appearance
+ * that you actually want.
+ *
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ */
+ public void setTypeface(Typeface tf) {
+ if (mTextPaint.getTypeface() != tf) {
+ mTextPaint.setTypeface(tf);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * @return the current typeface and style in which the text is being
+ * displayed.
+ */
+ public Typeface getTypeface() {
+ return mTextPaint.getTypeface();
+ }
+
+ /**
+ * Sets the text color for all the states (normal, selected,
+ * focused) to be this color.
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ @android.view.RemotableViewMethod
+ public void setTextColor(int color) {
+ mTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the text color.
+ *
+ * @attr ref android.R.styleable#TextView_textColor
+ */
+ public void setTextColor(ColorStateList colors) {
+ if (colors == null) {
+ throw new NullPointerException();
+ }
+
+ mTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * Return the set of text colors.
+ *
+ * @return Returns the set of text colors.
+ */
+ public final ColorStateList getTextColors() {
+ return mTextColor;
+ }
+
+ /**
+ * <p>Return the current color selected for normal text.</p>
+ *
+ * @return Returns the current text color.
+ */
+ public final int getCurrentTextColor() {
+ return mCurTextColor;
+ }
+
+ /**
+ * Sets the color used to display the selection highlight.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ */
+ @android.view.RemotableViewMethod
+ public void setHighlightColor(int color) {
+ if (mHighlightColor != color) {
+ mHighlightColor = color;
+ invalidate();
+ }
+ }
+
+ /**
+ * Gives the text a shadow of the specified radius and color, the specified
+ * distance from its normal position.
+ *
+ * @attr ref android.R.styleable#TextView_shadowColor
+ * @attr ref android.R.styleable#TextView_shadowDx
+ * @attr ref android.R.styleable#TextView_shadowDy
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ */
+ public void setShadowLayer(float radius, float dx, float dy, int color) {
+ mTextPaint.setShadowLayer(radius, dx, dy, color);
+
+ mShadowRadius = radius;
+ mShadowDx = dx;
+ mShadowDy = dy;
+
+ invalidate();
+ }
+
+ /**
+ * @return the base paint used for the text. Please use this only to
+ * consult the Paint's properties and not to change them.
+ */
+ public TextPaint getPaint() {
+ return mTextPaint;
+ }
+
+ /**
+ * Sets the autolink mask of the text. See {@link
+ * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+ * possible values.
+ *
+ * @attr ref android.R.styleable#TextView_autoLink
+ */
+ @android.view.RemotableViewMethod
+ public final void setAutoLinkMask(int mask) {
+ mAutoLinkMask = mask;
+ }
+
+ /**
+ * Sets whether the movement method will automatically be set to
+ * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
+ * set to nonzero and links are detected in {@link #setText}.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_linksClickable
+ */
+ @android.view.RemotableViewMethod
+ public final void setLinksClickable(boolean whether) {
+ mLinksClickable = whether;
+ }
+
+ /**
+ * Returns whether the movement method will automatically be set to
+ * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
+ * set to nonzero and links are detected in {@link #setText}.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_linksClickable
+ */
+ public final boolean getLinksClickable() {
+ return mLinksClickable;
+ }
+
+ /**
+ * Returns the list of URLSpans attached to the text
+ * (by {@link Linkify} or otherwise) if any. You can call
+ * {@link URLSpan#getURL} on them to find where they link to
+ * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
+ * to find the region of the text they are attached to.
+ */
+ public URLSpan[] getUrls() {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
+ } else {
+ return new URLSpan[0];
+ }
+ }
+
+ /**
+ * Sets the color of the hint text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ @android.view.RemotableViewMethod
+ public final void setHintTextColor(int color) {
+ mHintTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the color of the hint text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorHint
+ */
+ public final void setHintTextColor(ColorStateList colors) {
+ mHintTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * <p>Return the color used to paint the hint text.</p>
+ *
+ * @return Returns the list of hint text colors.
+ */
+ public final ColorStateList getHintTextColors() {
+ return mHintTextColor;
+ }
+
+ /**
+ * <p>Return the current color selected to paint the hint text.</p>
+ *
+ * @return Returns the current hint text color.
+ */
+ public final int getCurrentHintTextColor() {
+ return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ @android.view.RemotableViewMethod
+ public final void setLinkTextColor(int color) {
+ mLinkTextColor = ColorStateList.valueOf(color);
+ updateTextColors();
+ }
+
+ /**
+ * Sets the color of links in the text.
+ *
+ * @attr ref android.R.styleable#TextView_textColorLink
+ */
+ public final void setLinkTextColor(ColorStateList colors) {
+ mLinkTextColor = colors;
+ updateTextColors();
+ }
+
+ /**
+ * <p>Returns the color used to paint links in the text.</p>
+ *
+ * @return Returns the list of link text colors.
+ */
+ public final ColorStateList getLinkTextColors() {
+ return mLinkTextColor;
+ }
+
+ /**
+ * Sets the horizontal alignment of the text and the
+ * vertical gravity that will be used when there is extra space
+ * in the TextView beyond what is required for the text itself.
+ *
+ * @see android.view.Gravity
+ * @attr ref android.R.styleable#TextView_gravity
+ */
+ public void setGravity(int gravity) {
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.LEFT;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+ gravity |= Gravity.TOP;
+ }
+
+ boolean newLayout = false;
+
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) !=
+ (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
+ newLayout = true;
+ }
+
+ if (gravity != mGravity) {
+ invalidate();
+ }
+
+ mGravity = gravity;
+
+ if (mLayout != null && newLayout) {
+ // XXX this is heavy-handed because no actual content changes.
+ int want = mLayout.getWidth();
+ int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
+
+ makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
+ mRight - mLeft - getCompoundPaddingLeft() -
+ getCompoundPaddingRight(), true);
+ }
+ }
+
+ /**
+ * Returns the horizontal and vertical alignment of this TextView.
+ *
+ * @see android.view.Gravity
+ * @attr ref android.R.styleable#TextView_gravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * @return the flags on the Paint being used to display the text.
+ * @see Paint#getFlags
+ */
+ public int getPaintFlags() {
+ return mTextPaint.getFlags();
+ }
+
+ /**
+ * Sets flags on the Paint being used to display the text and
+ * reflows the text if they are different from the old flags.
+ * @see Paint#setFlags
+ */
+ @android.view.RemotableViewMethod
+ public void setPaintFlags(int flags) {
+ if (mTextPaint.getFlags() != flags) {
+ mTextPaint.setFlags(flags);
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Sets whether the text should be allowed to be wider than the
+ * View is. If false, it will be wrapped to the width of the View.
+ *
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ */
+ public void setHorizontallyScrolling(boolean whether) {
+ mHorizontallyScrolling = whether;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Makes the TextView at least this many lines tall
+ *
+ * @attr ref android.R.styleable#TextView_minLines
+ */
+ @android.view.RemotableViewMethod
+ public void setMinLines(int minlines) {
+ mMinimum = minlines;
+ mMinMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at least this many pixels tall
+ *
+ * @attr ref android.R.styleable#TextView_minHeight
+ */
+ @android.view.RemotableViewMethod
+ public void setMinHeight(int minHeight) {
+ mMinimum = minHeight;
+ mMinMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many lines tall
+ *
+ * @attr ref android.R.styleable#TextView_maxLines
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxLines(int maxlines) {
+ mMaximum = maxlines;
+ mMaxMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many pixels tall
+ *
+ * @attr ref android.R.styleable#TextView_maxHeight
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxHeight(int maxHeight) {
+ mMaximum = maxHeight;
+ mMaxMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many lines tall
+ *
+ * @attr ref android.R.styleable#TextView_lines
+ */
+ @android.view.RemotableViewMethod
+ public void setLines(int lines) {
+ mMaximum = mMinimum = lines;
+ mMaxMode = mMinMode = LINES;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many pixels tall.
+ * You could do the same thing by specifying this number in the
+ * LayoutParams.
+ *
+ * @attr ref android.R.styleable#TextView_height
+ */
+ @android.view.RemotableViewMethod
+ public void setHeight(int pixels) {
+ mMaximum = mMinimum = pixels;
+ mMaxMode = mMinMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at least this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_minEms
+ */
+ @android.view.RemotableViewMethod
+ public void setMinEms(int minems) {
+ mMinWidth = minems;
+ mMinWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at least this many pixels wide
+ *
+ * @attr ref android.R.styleable#TextView_minWidth
+ */
+ @android.view.RemotableViewMethod
+ public void setMinWidth(int minpixels) {
+ mMinWidth = minpixels;
+ mMinWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_maxEms
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxEms(int maxems) {
+ mMaxWidth = maxems;
+ mMaxWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView at most this many pixels wide
+ *
+ * @attr ref android.R.styleable#TextView_maxWidth
+ */
+ @android.view.RemotableViewMethod
+ public void setMaxWidth(int maxpixels) {
+ mMaxWidth = maxpixels;
+ mMaxWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many ems wide
+ *
+ * @attr ref android.R.styleable#TextView_ems
+ */
+ @android.view.RemotableViewMethod
+ public void setEms(int ems) {
+ mMaxWidth = mMinWidth = ems;
+ mMaxWidthMode = mMinWidthMode = EMS;
+
+ requestLayout();
+ invalidate();
+ }
+
+ /**
+ * Makes the TextView exactly this many pixels wide.
+ * You could do the same thing by specifying this number in the
+ * LayoutParams.
+ *
+ * @attr ref android.R.styleable#TextView_width
+ */
+ @android.view.RemotableViewMethod
+ public void setWidth(int pixels) {
+ mMaxWidth = mMinWidth = pixels;
+ mMaxWidthMode = mMinWidthMode = PIXELS;
+
+ requestLayout();
+ invalidate();
+ }
+
+
+ /**
+ * Sets line spacing for this TextView. Each line will have its height
+ * multiplied by <code>mult</code> and have <code>add</code> added to it.
+ *
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ */
+ public void setLineSpacing(float add, float mult) {
+ mSpacingMult = mult;
+ mSpacingAdd = add;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Convenience method: Append the specified text to the TextView's
+ * display buffer, upgrading it to BufferType.EDITABLE if it was
+ * not already editable.
+ */
+ public final void append(CharSequence text) {
+ append(text, 0, text.length());
+ }
+
+ /**
+ * Convenience method: Append the specified text slice to the TextView's
+ * display buffer, upgrading it to BufferType.EDITABLE if it was
+ * not already editable.
+ */
+ public void append(CharSequence text, int start, int end) {
+ if (!(mText instanceof Editable)) {
+ setText(mText, BufferType.EDITABLE);
+ }
+
+ ((Editable) mText).append(text, start, end);
+ }
+
+ private void updateTextColors() {
+ boolean inval = false;
+ int color = mTextColor.getColorForState(getDrawableState(), 0);
+ if (color != mCurTextColor) {
+ mCurTextColor = color;
+ inval = true;
+ }
+ if (mLinkTextColor != null) {
+ color = mLinkTextColor.getColorForState(getDrawableState(), 0);
+ if (color != mTextPaint.linkColor) {
+ mTextPaint.linkColor = color;
+ inval = true;
+ }
+ }
+ if (mHintTextColor != null) {
+ color = mHintTextColor.getColorForState(getDrawableState(), 0);
+ if (color != mCurHintTextColor && mText.length() == 0) {
+ mCurHintTextColor = color;
+ inval = true;
+ }
+ }
+ if (inval) {
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mTextColor != null && mTextColor.isStateful()
+ || (mHintTextColor != null && mHintTextColor.isStateful())
+ || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
+ updateTextColors();
+ }
+
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ int[] state = getDrawableState();
+ if (dr.mDrawableTop != null && dr.mDrawableTop.isStateful()) {
+ dr.mDrawableTop.setState(state);
+ }
+ if (dr.mDrawableBottom != null && dr.mDrawableBottom.isStateful()) {
+ dr.mDrawableBottom.setState(state);
+ }
+ if (dr.mDrawableLeft != null && dr.mDrawableLeft.isStateful()) {
+ dr.mDrawableLeft.setState(state);
+ }
+ if (dr.mDrawableRight != null && dr.mDrawableRight.isStateful()) {
+ dr.mDrawableRight.setState(state);
+ }
+ }
+ }
+
+ /**
+ * User interface state that is stored by TextView for implementing
+ * {@link View#onSaveInstanceState}.
+ */
+ public static class SavedState extends BaseSavedState {
+ int selStart;
+ int selEnd;
+ CharSequence text;
+ boolean frozenWithFocus;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(selStart);
+ out.writeInt(selEnd);
+ out.writeInt(frozenWithFocus ? 1 : 0);
+ TextUtils.writeToParcel(text, out, flags);
+ }
+
+ @Override
+ public String toString() {
+ String str = "TextView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " start=" + selStart + " end=" + selEnd;
+ if (text != null) {
+ str += " text=" + text;
+ }
+ return str + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ private SavedState(Parcel in) {
+ super(in);
+ selStart = in.readInt();
+ selEnd = in.readInt();
+ frozenWithFocus = (in.readInt() != 0);
+ text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+
+ // Save state if we are forced to
+ boolean save = mFreezesText;
+ int start = 0;
+ int end = 0;
+
+ if (mText != null) {
+ start = Selection.getSelectionStart(mText);
+ end = Selection.getSelectionEnd(mText);
+ if (start >= 0 || end >= 0) {
+ // Or save state if there is a selection
+ save = true;
+ }
+ }
+
+ if (save) {
+ SavedState ss = new SavedState(superState);
+ // XXX Should also save the current scroll position!
+ ss.selStart = start;
+ ss.selEnd = end;
+
+ if (mText instanceof Spanned) {
+ /*
+ * Calling setText() strips off any ChangeWatchers;
+ * strip them now to avoid leaking references.
+ * But do it to a copy so that if there are any
+ * further changes to the text of this view, it
+ * won't get into an inconsistent state.
+ */
+
+ Spannable sp = new SpannableString(mText);
+
+ for (ChangeWatcher cw :
+ sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
+ sp.removeSpan(cw);
+ }
+
+ ss.text = sp;
+ } else {
+ ss.text = mText.toString();
+ }
+
+ if (isFocused() && start >= 0 && end >= 0) {
+ ss.frozenWithFocus = true;
+ }
+
+ return ss;
+ }
+
+ return superState;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ SavedState ss = (SavedState)state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ // XXX restore buffer type too, as well as lots of other stuff
+ if (ss.text != null) {
+ setText(ss.text);
+ }
+
+ if (ss.selStart >= 0 && ss.selEnd >= 0) {
+ if (mText instanceof Spannable) {
+ int len = mText.length();
+
+ if (ss.selStart > len || ss.selEnd > len) {
+ String restored = "";
+
+ if (ss.text != null) {
+ restored = "(restored) ";
+ }
+
+ Log.e("TextView", "Saved cursor position " + ss.selStart +
+ "/" + ss.selEnd + " out of range for " + restored +
+ "text " + mText);
+ } else {
+ Selection.setSelection((Spannable) mText, ss.selStart,
+ ss.selEnd);
+
+ if (ss.frozenWithFocus) {
+ mFrozenWithFocus = true;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Control whether this text view saves its entire text contents when
+ * freezing to an icicle, in addition to dynamic state such as cursor
+ * position. By default this is false, not saving the text. Set to true
+ * if the text in the text view is not being saved somewhere else in
+ * persistent storage (such as in a content provider) so that if the
+ * view is later thawed the user will not lose their data.
+ *
+ * @param freezesText Controls whether a frozen icicle should include the
+ * entire text data: true to include it, false to not.
+ *
+ * @attr ref android.R.styleable#TextView_freezesText
+ */
+ @android.view.RemotableViewMethod
+ public void setFreezesText(boolean freezesText) {
+ mFreezesText = freezesText;
+ }
+
+ /**
+ * Return whether this text view is including its entire text contents
+ * in frozen icicles.
+ *
+ * @return Returns true if text is included, false if it isn't.
+ *
+ * @see #setFreezesText
+ */
+ public boolean getFreezesText() {
+ return mFreezesText;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Sets the Factory used to create new Editables.
+ */
+ public final void setEditableFactory(Editable.Factory factory) {
+ mEditableFactory = factory;
+ setText(mText);
+ }
+
+ /**
+ * Sets the Factory used to create new Spannables.
+ */
+ public final void setSpannableFactory(Spannable.Factory factory) {
+ mSpannableFactory = factory;
+ setText(mText);
+ }
+
+ /**
+ * Sets the string value of the TextView. TextView <em>does not</em> accept
+ * HTML-like formatting, which you can do with text strings in XML resource files.
+ * To style your strings, attach android.text.style.* objects to a
+ * {@link android.text.SpannableString SpannableString}, or see the
+ * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
+ * Available Resource Types</a> documentation for an example of setting
+ * formatted text in the XML resource file.
+ *
+ * @attr ref android.R.styleable#TextView_text
+ */
+ @android.view.RemotableViewMethod
+ public final void setText(CharSequence text) {
+ setText(text, mBufferType);
+ }
+
+ /**
+ * Like {@link #setText(CharSequence)},
+ * except that the cursor position (if any) is retained in the new text.
+ *
+ * @param text The new text to place in the text view.
+ *
+ * @see #setText(CharSequence)
+ */
+ @android.view.RemotableViewMethod
+ public final void setTextKeepState(CharSequence text) {
+ setTextKeepState(text, mBufferType);
+ }
+
+ /**
+ * Sets the text that this TextView is to display (see
+ * {@link #setText(CharSequence)}) and also sets whether it is stored
+ * in a styleable/spannable buffer and whether it is editable.
+ *
+ * @attr ref android.R.styleable#TextView_text
+ * @attr ref android.R.styleable#TextView_bufferType
+ */
+ public void setText(CharSequence text, BufferType type) {
+ setText(text, type, true, 0);
+
+ if (mCharWrapper != null) {
+ mCharWrapper.mChars = null;
+ }
+ }
+
+ private void setText(CharSequence text, BufferType type,
+ boolean notifyBefore, int oldlen) {
+ if (text == null) {
+ text = "";
+ }
+
+ if (text instanceof Spanned &&
+ ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
+ setHorizontalFadingEdgeEnabled(true);
+ setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ }
+
+ int n = mFilters.length;
+ for (int i = 0; i < n; i++) {
+ CharSequence out = mFilters[i].filter(text, 0, text.length(),
+ EMPTY_SPANNED, 0, 0);
+ if (out != null) {
+ text = out;
+ }
+ }
+
+ if (notifyBefore) {
+ if (mText != null) {
+ oldlen = mText.length();
+ sendBeforeTextChanged(mText, 0, oldlen, text.length());
+ } else {
+ sendBeforeTextChanged("", 0, 0, text.length());
+ }
+ }
+
+ boolean needEditableForNotification = false;
+
+ if (mListeners != null && mListeners.size() != 0) {
+ needEditableForNotification = true;
+ }
+
+ if (type == BufferType.EDITABLE || mInput != null ||
+ needEditableForNotification) {
+ Editable t = mEditableFactory.newEditable(text);
+ text = t;
+ setFilters(t, mFilters);
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) imm.restartInput(this);
+ } else if (type == BufferType.SPANNABLE || mMovement != null) {
+ text = mSpannableFactory.newSpannable(text);
+ } else if (!(text instanceof CharWrapper)) {
+ text = TextUtils.stringOrSpannedString(text);
+ }
+
+ if (mAutoLinkMask != 0) {
+ Spannable s2;
+
+ if (type == BufferType.EDITABLE || text instanceof Spannable) {
+ s2 = (Spannable) text;
+ } else {
+ s2 = mSpannableFactory.newSpannable(text);
+ }
+
+ if (Linkify.addLinks(s2, mAutoLinkMask)) {
+ text = s2;
+ type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
+
+ /*
+ * We must go ahead and set the text before changing the
+ * movement method, because setMovementMethod() may call
+ * setText() again to try to upgrade the buffer type.
+ */
+ mText = text;
+
+ if (mLinksClickable) {
+ setMovementMethod(LinkMovementMethod.getInstance());
+ }
+ }
+ }
+
+ mBufferType = type;
+ mText = text;
+
+ if (mTransformation == null)
+ mTransformed = text;
+ else
+ mTransformed = mTransformation.getTransformation(text, this);
+
+ final int textLength = text.length();
+
+ if (text instanceof Spannable) {
+ Spannable sp = (Spannable) text;
+
+ // Remove any ChangeWatchers that might have come
+ // from other TextViews.
+ final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
+ final int count = watchers.length;
+ for (int i = 0; i < count; i++)
+ sp.removeSpan(watchers[i]);
+
+ if (mChangeWatcher == null)
+ mChangeWatcher = new ChangeWatcher();
+
+ sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
+ (PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
+
+ if (mInput != null) {
+ sp.setSpan(mInput, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ if (mTransformation != null) {
+ sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ }
+
+ if (mMovement != null) {
+ mMovement.initialize(this, (Spannable) text);
+
+ /*
+ * Initializing the movement method will have set the
+ * selection, so reset mSelectionMoved to keep that from
+ * interfering with the normal on-focus selection-setting.
+ */
+ mSelectionMoved = false;
+ }
+ }
+
+ if (mLayout != null) {
+ checkForRelayout();
+ }
+
+ sendOnTextChanged(text, 0, oldlen, textLength);
+ onTextChanged(text, 0, oldlen, textLength);
+
+ if (needEditableForNotification) {
+ sendAfterTextChanged((Editable) text);
+ }
+ }
+
+ /**
+ * Sets the TextView to display the specified slice of the specified
+ * char array. You must promise that you will not change the contents
+ * of the array except for right before another call to setText(),
+ * since the TextView has no way to know that the text
+ * has changed and that it needs to invalidate and re-layout.
+ */
+ public final void setText(char[] text, int start, int len) {
+ int oldlen = 0;
+
+ if (start < 0 || len < 0 || start + len > text.length) {
+ throw new IndexOutOfBoundsException(start + ", " + len);
+ }
+
+ /*
+ * We must do the before-notification here ourselves because if
+ * the old text is a CharWrapper we destroy it before calling
+ * into the normal path.
+ */
+ if (mText != null) {
+ oldlen = mText.length();
+ sendBeforeTextChanged(mText, 0, oldlen, len);
+ } else {
+ sendBeforeTextChanged("", 0, 0, len);
+ }
+
+ if (mCharWrapper == null) {
+ mCharWrapper = new CharWrapper(text, start, len);
+ } else {
+ mCharWrapper.set(text, start, len);
+ }
+
+ setText(mCharWrapper, mBufferType, false, oldlen);
+ }
+
+ private static class CharWrapper
+ implements CharSequence, GetChars, GraphicsOperations {
+ private char[] mChars;
+ private int mStart, mLength;
+
+ public CharWrapper(char[] chars, int start, int len) {
+ mChars = chars;
+ mStart = start;
+ mLength = len;
+ }
+
+ /* package */ void set(char[] chars, int start, int len) {
+ mChars = chars;
+ mStart = start;
+ mLength = len;
+ }
+
+ public int length() {
+ return mLength;
+ }
+
+ public char charAt(int off) {
+ return mChars[off + mStart];
+ }
+
+ public String toString() {
+ return new String(mChars, mStart, mLength);
+ }
+
+ public CharSequence subSequence(int start, int end) {
+ if (start < 0 || end < 0 || start > mLength || end > mLength) {
+ throw new IndexOutOfBoundsException(start + ", " + end);
+ }
+
+ return new String(mChars, start + mStart, end - start);
+ }
+
+ public void getChars(int start, int end, char[] buf, int off) {
+ if (start < 0 || end < 0 || start > mLength || end > mLength) {
+ throw new IndexOutOfBoundsException(start + ", " + end);
+ }
+
+ System.arraycopy(mChars, start + mStart, buf, off, end - start);
+ }
+
+ public void drawText(Canvas c, int start, int end,
+ float x, float y, Paint p) {
+ c.drawText(mChars, start + mStart, end - start, x, y, p);
+ }
+
+ public float measureText(int start, int end, Paint p) {
+ return p.measureText(mChars, start + mStart, end - start);
+ }
+
+ public int getTextWidths(int start, int end, float[] widths, Paint p) {
+ return p.getTextWidths(mChars, start + mStart, end - start, widths);
+ }
+ }
+
+ /**
+ * Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
+ * except that the cursor position (if any) is retained in the new text.
+ *
+ * @see #setText(CharSequence, android.widget.TextView.BufferType)
+ */
+ public final void setTextKeepState(CharSequence text, BufferType type) {
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ int len = text.length();
+
+ setText(text, type);
+
+ if (start >= 0 || end >= 0) {
+ if (mText instanceof Spannable) {
+ Selection.setSelection((Spannable) mText,
+ Math.max(0, Math.min(start, len)),
+ Math.max(0, Math.min(end, len)));
+ }
+ }
+ }
+
+ @android.view.RemotableViewMethod
+ public final void setText(int resid) {
+ setText(getContext().getResources().getText(resid));
+ }
+
+ public final void setText(int resid, BufferType type) {
+ setText(getContext().getResources().getText(resid), type);
+ }
+
+ /**
+ * Sets the text to be displayed when the text of the TextView is empty.
+ * Null means to use the normal empty text. The hint does not currently
+ * participate in determining the size of the view.
+ *
+ * This method is deprecated. Use {link #setHint(int, String)} or
+ * {link #setHint(CharSequence, String)} instead.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ @android.view.RemotableViewMethod
+ public final void setHint(CharSequence hint) {
+ mHint = TextUtils.stringOrSpannedString(hint);
+
+ if (mLayout != null) {
+ checkForRelayout();
+ }
+
+ if (mText.length() == 0)
+ invalidate();
+ }
+
+ /**
+ * Sets the text to be displayed when the text of the TextView is empty,
+ * from a resource.
+ *
+ * This method is deprecated. Use {link #setHint(int, String)} or
+ * {link #setHint(CharSequence, String)} instead.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ @android.view.RemotableViewMethod
+ public final void setHint(int resid) {
+ setHint(getContext().getResources().getText(resid));
+ }
+
+ /**
+ * Returns the hint that is displayed when the text of the TextView
+ * is empty.
+ *
+ * @attr ref android.R.styleable#TextView_hint
+ */
+ @ViewDebug.CapturedViewProperty
+ public CharSequence getHint() {
+ return mHint;
+ }
+
+ /**
+ * Set the type of the content with a constant as defined for
+ * {@link EditorInfo#inputType}. This will take care of changing
+ * the key listener, by calling {@link #setKeyListener(KeyListener)}, to
+ * match the given content type. If the given content type is
+ * {@link EditorInfo#TYPE_NULL} then a soft keyboard will
+ * not be displayed for this text view.
+ *
+ * @see #getInputType()
+ * @see #setRawInputType(int)
+ * @see android.text.InputType
+ * @attr ref android.R.styleable#TextView_inputType
+ */
+ public void setInputType(int type) {
+ setInputType(type, false);
+ final boolean isPassword = (type&(EditorInfo.TYPE_MASK_CLASS
+ |EditorInfo.TYPE_MASK_VARIATION))
+ == (EditorInfo.TYPE_CLASS_TEXT
+ |EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
+ boolean forceUpdate = false;
+ if (isPassword) {
+ setTransformationMethod(PasswordTransformationMethod.getInstance());
+ setTypefaceByIndex(MONOSPACE, 0);
+ } else if (mTransformation == PasswordTransformationMethod.getInstance()) {
+ // We need to clean up if we were previously in password mode.
+ setTypefaceByIndex(-1, -1);
+ forceUpdate = true;
+ }
+
+ boolean multiLine = (type&(EditorInfo.TYPE_MASK_CLASS
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
+ (EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
+
+ // We need to update the single line mode if it has changed or we
+ // were previously in password mode.
+ if (mSingleLine == multiLine || forceUpdate) {
+ // Change single line mode, but only change the transformation if
+ // we are not in password mode.
+ applySingleLine(!multiLine, !isPassword);
+ }
+
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) imm.restartInput(this);
+ }
+
+ /**
+ * Directly change the content type integer of the text view, without
+ * modifying any other state.
+ * @see #setInputType(int)
+ * @see android.text.InputType
+ * @attr ref android.R.styleable#TextView_inputType
+ */
+ public void setRawInputType(int type) {
+ mInputType = type;
+ }
+
+ private void setInputType(int type, boolean direct) {
+ final int cls = type & EditorInfo.TYPE_MASK_CLASS;
+ KeyListener input;
+ if (cls == EditorInfo.TYPE_CLASS_TEXT) {
+ boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT)
+ != 0;
+ TextKeyListener.Capitalize cap;
+ if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
+ cap = TextKeyListener.Capitalize.CHARACTERS;
+ } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
+ cap = TextKeyListener.Capitalize.WORDS;
+ } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
+ cap = TextKeyListener.Capitalize.SENTENCES;
+ } else {
+ cap = TextKeyListener.Capitalize.NONE;
+ }
+ input = TextKeyListener.getInstance(autotext, cap);
+ } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
+ input = DigitsKeyListener.getInstance(
+ (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
+ (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
+ } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
+ switch (type & EditorInfo.TYPE_MASK_VARIATION) {
+ case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
+ input = DateKeyListener.getInstance();
+ break;
+ case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
+ input = TimeKeyListener.getInstance();
+ break;
+ default:
+ input = DateTimeKeyListener.getInstance();
+ break;
+ }
+ } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
+ input = DialerKeyListener.getInstance();
+ } else {
+ input = TextKeyListener.getInstance();
+ }
+ mInputType = type;
+ if (direct) mInput = input;
+ else {
+ setKeyListenerOnly(input);
+ }
+ }
+
+ /**
+ * Get the type of the content.
+ *
+ * @see #setInputType(int)
+ * @see android.text.InputType
+ */
+ public int getInputType() {
+ return mInputType;
+ }
+
+ /**
+ * Change the editor type integer associated with the text view, which
+ * will be reported to an IME with {@link EditorInfo#imeOptions} when it
+ * has focus.
+ * @see #getImeOptions
+ * @see android.view.inputmethod.EditorInfo
+ * @attr ref android.R.styleable#TextView_imeOptions
+ */
+ public void setImeOptions(int imeOptions) {
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ mInputContentType.imeOptions = imeOptions;
+ }
+
+ /**
+ * Get the type of the IME editor.
+ *
+ * @see #setImeOptions(int)
+ * @see android.view.inputmethod.EditorInfo
+ */
+ public int getImeOptions() {
+ return mInputContentType != null
+ ? mInputContentType.imeOptions : EditorInfo.IME_UNDEFINED;
+ }
+
+ /**
+ * Change the custom IME action associated with the text view, which
+ * will be reported to an IME with {@link EditorInfo#actionLabel}
+ * and {@link EditorInfo#actionId} when it has focus.
+ * @see #getImeActionLabel
+ * @see #getImeActionId
+ * @see android.view.inputmethod.EditorInfo
+ * @attr ref android.R.styleable#TextView_imeActionLabel
+ * @attr ref android.R.styleable#TextView_imeActionId
+ */
+ public void setImeActionLabel(CharSequence label, int actionId) {
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ mInputContentType.imeActionLabel = label;
+ mInputContentType.imeActionId = actionId;
+ }
+
+ /**
+ * Get the IME action label previous set with {@link #setImeActionLabel}.
+ *
+ * @see #setImeActionLabel
+ * @see android.view.inputmethod.EditorInfo
+ */
+ public CharSequence getImeActionLabel() {
+ return mInputContentType != null
+ ? mInputContentType.imeActionLabel : null;
+ }
+
+ /**
+ * Get the IME action ID previous set with {@link #setImeActionLabel}.
+ *
+ * @see #setImeActionLabel
+ * @see android.view.inputmethod.EditorInfo
+ */
+ public int getImeActionId() {
+ return mInputContentType != null
+ ? mInputContentType.imeActionId : 0;
+ }
+
+ /**
+ * Set a special listener to be called when an action is performed
+ * on the text view. This will be called when the enter key is pressed,
+ * or when an action supplied to the IME is selected by the user. Setting
+ * this means that the normal hard key event will not insert a newline
+ * into the text view, even if it is multi-line; holding down the ALT
+ * modifier will, however, allow the user to insert a newline character.
+ */
+ public void setOnEditorActionListener(OnEditorActionListener l) {
+ if (mInputContentType == null) {
+ mInputContentType = new InputContentType();
+ }
+ mInputContentType.onEditorActionListener = l;
+ }
+
+ /**
+ * Called when an attached input method calls
+ * {@link InputConnection#performEditorAction(int)
+ * InputConnection.performEditorAction()}
+ * for this text view. The default implementation will call your click
+ * listener supplied to {@link #setOnEditorActionListener},
+ * or generate an enter key down/up pair to invoke the action if not.
+ *
+ * @param actionCode The code of the action being performed.
+ *
+ * @see #setOnEditorActionListener
+ */
+ public void onEditorAction(int actionCode) {
+ final InputContentType ict = mInputContentType;
+ if (ict != null) {
+ if (ict.onEditorActionListener != null) {
+ if (ict.onEditorActionListener.onEditorAction(this,
+ actionCode, null)) {
+ return;
+ }
+ }
+ }
+
+ if (actionCode == EditorInfo.IME_ACTION_NEXT &&
+ (ict != null || !shouldAdvanceFocusOnEnter())) {
+ // This is the default handling for the NEXT action, to advance
+ // focus. Note that for backwards compatibility we don't do this
+ // default handling if explicit ime options have not been given,
+ // and we do not advance by default on an enter key -- in that
+ // case, we want to turn this into the normal enter key codes that
+ // an app may be expecting.
+ View v = focusSearch(FOCUS_DOWN);
+ if (v != null) {
+ if (!v.requestFocus(FOCUS_DOWN)) {
+ throw new IllegalStateException("focus search returned a view " +
+ "that wasn't able to take focus!");
+ }
+ }
+ return;
+ }
+
+ Handler h = getHandler();
+ long eventTime = SystemClock.uptimeMillis();
+ h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME,
+ new KeyEvent(eventTime, eventTime,
+ KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)));
+ h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME,
+ new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+ KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0,
+ KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE)));
+ }
+
+ /**
+ * Set the private content type of the text, which is the
+ * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
+ * field that will be filled in when creating an input connection.
+ *
+ * @see #getPrivateImeOptions()
+ * @see EditorInfo#privateImeOptions
+ * @attr ref android.R.styleable#TextView_privateImeOptions
+ */
+ public void setPrivateImeOptions(String type) {
+ if (mInputContentType == null) mInputContentType = new InputContentType();
+ mInputContentType.privateImeOptions = type;
+ }
+
+ /**
+ * Get the private type of the content.
+ *
+ * @see #setPrivateImeOptions(String)
+ * @see EditorInfo#privateImeOptions
+ */
+ public String getPrivateImeOptions() {
+ return mInputContentType != null
+ ? mInputContentType.privateImeOptions : null;
+ }
+
+ /**
+ * Set the extra input data of the text, which is the
+ * {@link EditorInfo#extras TextBoxAttribute.extras}
+ * Bundle that will be filled in when creating an input connection. The
+ * given integer is the resource ID of an XML resource holding an
+ * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
+ *
+ * @see #getInputExtras(boolean)
+ * @see EditorInfo#extras
+ * @attr ref android.R.styleable#TextView_editorExtras
+ */
+ public void setInputExtras(int xmlResId)
+ throws XmlPullParserException, IOException {
+ XmlResourceParser parser = getResources().getXml(xmlResId);
+ if (mInputContentType == null) mInputContentType = new InputContentType();
+ mInputContentType.extras = new Bundle();
+ getResources().parseBundleExtras(parser, mInputContentType.extras);
+ }
+
+ /**
+ * Retrieve the input extras currently associated with the text view, which
+ * can be viewed as well as modified.
+ *
+ * @param create If true, the extras will be created if they don't already
+ * exist. Otherwise, null will be returned if none have been created.
+ * @see #setInputExtras(int)View
+ * @see EditorInfo#extras
+ * @attr ref android.R.styleable#TextView_editorExtras
+ */
+ public Bundle getInputExtras(boolean create) {
+ if (mInputContentType == null) {
+ if (!create) return null;
+ mInputContentType = new InputContentType();
+ }
+ if (mInputContentType.extras == null) {
+ if (!create) return null;
+ mInputContentType.extras = new Bundle();
+ }
+ return mInputContentType.extras;
+ }
+
+ /**
+ * Returns the error message that was set to be displayed with
+ * {@link #setError}, or <code>null</code> if no error was set
+ * or if it the error was cleared by the widget after user input.
+ */
+ public CharSequence getError() {
+ return mError;
+ }
+
+ /**
+ * Sets the right-hand compound drawable of the TextView to the "error"
+ * icon and sets an error message that will be displayed in a popup when
+ * the TextView has focus. The icon and error message will be reset to
+ * null when any key events cause changes to the TextView's text. If the
+ * <code>error</code> is <code>null</code>, the error message and icon
+ * will be cleared.
+ */
+ @android.view.RemotableViewMethod
+ public void setError(CharSequence error) {
+ if (error == null) {
+ setError(null, null);
+ } else {
+ Drawable dr = getContext().getResources().
+ getDrawable(com.android.internal.R.drawable.
+ indicator_input_error);
+
+ dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
+ setError(error, dr);
+ }
+ }
+
+ /**
+ * Sets the right-hand compound drawable of the TextView to the specified
+ * icon and sets an error message that will be displayed in a popup when
+ * the TextView has focus. The icon and error message will be reset to
+ * null when any key events cause changes to the TextView's text. The
+ * drawable must already have had {@link Drawable#setBounds} set on it.
+ * If the <code>error</code> is <code>null</code>, the error message will
+ * be cleared (and you should provide a <code>null</code> icon as well).
+ */
+ public void setError(CharSequence error, Drawable icon) {
+ error = TextUtils.stringOrSpannedString(error);
+
+ mError = error;
+ mErrorWasChanged = true;
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop,
+ icon, dr.mDrawableBottom);
+ } else {
+ setCompoundDrawables(null, null, icon, null);
+ }
+
+ if (error == null) {
+ if (mPopup != null) {
+ if (mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+
+ mPopup = null;
+ }
+ } else {
+ if (isFocused()) {
+ showError();
+ }
+ }
+ }
+
+ private void showError() {
+ if (getWindowToken() == null) {
+ mShowErrorAfterAttach = true;
+ return;
+ }
+
+ if (mPopup == null) {
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ final TextView err = (TextView) inflater.inflate(com.android.internal.R.layout.textview_hint,
+ null);
+
+ mPopup = new PopupWindow(err, 200, 50) {
+ private boolean mAbove = false;
+
+ @Override
+ public void update(int x, int y, int w, int h, boolean force) {
+ super.update(x, y, w, h, force);
+
+ boolean above = isAboveAnchor();
+ if (above != mAbove) {
+ mAbove = above;
+
+ if (above) {
+ err.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error_above);
+ } else {
+ err.setBackgroundResource(com.android.internal.R.drawable.popup_inline_error);
+ }
+ }
+ }
+ };
+ mPopup.setFocusable(false);
+ }
+
+ TextView tv = (TextView) mPopup.getContentView();
+ chooseSize(mPopup, mError, tv);
+ tv.setText(mError);
+
+ mPopup.showAsDropDown(this, getErrorX(), getErrorY());
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the middle of the error icon.
+ */
+ private int getErrorX() {
+ /*
+ * The "25" is the distance between the point and the right edge
+ * of the background
+ */
+
+ final Drawables dr = mDrawables;
+ return getWidth() - mPopup.getWidth()
+ - getPaddingRight()
+ - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + 25;
+ }
+
+ /**
+ * Returns the Y offset to make the pointy top of the error point
+ * at the bottom of the error icon.
+ */
+ private int getErrorY() {
+ /*
+ * Compound, not extended, because the icon is not clipped
+ * if the text height is smaller.
+ */
+ int vspace = mBottom - mTop -
+ getCompoundPaddingBottom() - getCompoundPaddingTop();
+
+ final Drawables dr = mDrawables;
+ int icontop = getCompoundPaddingTop()
+ + (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
+
+ /*
+ * The "2" is the distance between the point and the top edge
+ * of the background.
+ */
+
+ return icontop + (dr != null ? dr.mDrawableHeightRight : 0)
+ - getHeight() - 2;
+ }
+
+ private void hideError() {
+ if (mPopup != null) {
+ if (mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ }
+
+ mShowErrorAfterAttach = false;
+ }
+
+ private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
+ int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+ int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+ /*
+ * Figure out how big the text would be if we laid it out to the
+ * full width of this view minus the border.
+ */
+ int cap = getWidth() - wid;
+ if (cap < 0) {
+ cap = 200; // We must not be measured yet -- setFrame() will fix it.
+ }
+
+ Layout l = new StaticLayout(text, tv.getPaint(), cap,
+ Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
+ float max = 0;
+ for (int i = 0; i < l.getLineCount(); i++) {
+ max = Math.max(max, l.getLineWidth(i));
+ }
+
+ /*
+ * Now set the popup size to be big enough for the text plus the border.
+ */
+ pop.setWidth(wid + (int) Math.ceil(max));
+ pop.setHeight(ht + l.getHeight());
+ }
+
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean result = super.setFrame(l, t, r, b);
+
+ if (mPopup != null) {
+ TextView tv = (TextView) mPopup.getContentView();
+ chooseSize(mPopup, mError, tv);
+ mPopup.update(this, getErrorX(), getErrorY(), -1, -1);
+ }
+
+ if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ mRestartMarquee = false;
+ startMarquee();
+ }
+
+ return result;
+ }
+
+ /**
+ * Sets the list of input filters that will be used if the buffer is
+ * Editable. Has no effect otherwise.
+ *
+ * @attr ref android.R.styleable#TextView_maxLength
+ */
+ public void setFilters(InputFilter[] filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException();
+ }
+
+ mFilters = filters;
+
+ if (mText instanceof Editable) {
+ setFilters((Editable) mText, filters);
+ }
+ }
+
+ /**
+ * Sets the list of input filters on the specified Editable,
+ * and includes mInput in the list if it is an InputFilter.
+ */
+ private void setFilters(Editable e, InputFilter[] filters) {
+ if (mInput instanceof InputFilter) {
+ InputFilter[] nf = new InputFilter[filters.length + 1];
+
+ System.arraycopy(filters, 0, nf, 0, filters.length);
+ nf[filters.length] = (InputFilter) mInput;
+
+ e.setFilters(nf);
+ } else {
+ e.setFilters(filters);
+ }
+ }
+
+ /**
+ * Returns the current list of input filters.
+ */
+ public InputFilter[] getFilters() {
+ return mFilters;
+ }
+
+ /////////////////////////////////////////////////////////////////////////
+
+ private int getVerticalOffset(boolean forceNormal) {
+ int voffset = 0;
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ Layout l = mLayout;
+ if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
+ l = mHintLayout;
+ }
+
+ if (gravity != Gravity.TOP) {
+ int boxht;
+
+ if (l == mHintLayout) {
+ boxht = getMeasuredHeight() - getCompoundPaddingTop() -
+ getCompoundPaddingBottom();
+ } else {
+ boxht = getMeasuredHeight() - getExtendedPaddingTop() -
+ getExtendedPaddingBottom();
+ }
+ int textht = l.getHeight();
+
+ if (textht < boxht) {
+ if (gravity == Gravity.BOTTOM)
+ voffset = boxht - textht;
+ else // (gravity == Gravity.CENTER_VERTICAL)
+ voffset = (boxht - textht) >> 1;
+ }
+ }
+ return voffset;
+ }
+
+ private int getBottomVerticalOffset(boolean forceNormal) {
+ int voffset = 0;
+ final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ Layout l = mLayout;
+ if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
+ l = mHintLayout;
+ }
+
+ if (gravity != Gravity.BOTTOM) {
+ int boxht;
+
+ if (l == mHintLayout) {
+ boxht = getMeasuredHeight() - getCompoundPaddingTop() -
+ getCompoundPaddingBottom();
+ } else {
+ boxht = getMeasuredHeight() - getExtendedPaddingTop() -
+ getExtendedPaddingBottom();
+ }
+ int textht = l.getHeight();
+
+ if (textht < boxht) {
+ if (gravity == Gravity.TOP)
+ voffset = boxht - textht;
+ else // (gravity == Gravity.CENTER_VERTICAL)
+ voffset = (boxht - textht) >> 1;
+ }
+ }
+ return voffset;
+ }
+
+ private void invalidateCursorPath() {
+ if (mHighlightPathBogus) {
+ invalidateCursor();
+ } else {
+ synchronized (sTempRect) {
+ /*
+ * The reason for this concern about the thickness of the
+ * cursor and doing the floor/ceil on the coordinates is that
+ * some EditTexts (notably textfields in the Browser) have
+ * anti-aliased text where not all the characters are
+ * necessarily at integer-multiple locations. This should
+ * make sure the entire cursor gets invalidated instead of
+ * sometimes missing half a pixel.
+ */
+
+ float thick = FloatMath.ceil(mTextPaint.getStrokeWidth());
+ if (thick < 1.0f) {
+ thick = 1.0f;
+ }
+
+ thick /= 2;
+
+ mHighlightPath.computeBounds(sTempRect, false);
+
+ int left = getCompoundPaddingLeft();
+ int top = getExtendedPaddingTop() + getVerticalOffset(true);
+
+ invalidate((int) FloatMath.floor(left + sTempRect.left - thick),
+ (int) FloatMath.floor(top + sTempRect.top - thick),
+ (int) FloatMath.ceil(left + sTempRect.right + thick),
+ (int) FloatMath.ceil(top + sTempRect.bottom + thick));
+ }
+ }
+ }
+
+ private void invalidateCursor() {
+ int where = Selection.getSelectionEnd(mText);
+
+ invalidateCursor(where, where, where);
+ }
+
+ private void invalidateCursor(int a, int b, int c) {
+ if (mLayout == null) {
+ invalidate();
+ } else {
+ if (a >= 0 || b >= 0 || c >= 0) {
+ int first = Math.min(Math.min(a, b), c);
+ int last = Math.max(Math.max(a, b), c);
+
+ int line = mLayout.getLineForOffset(first);
+ int top = mLayout.getLineTop(line);
+
+ // This is ridiculous, but the descent from the line above
+ // can hang down into the line we really want to redraw,
+ // so we have to invalidate part of the line above to make
+ // sure everything that needs to be redrawn really is.
+ // (But not the whole line above, because that would cause
+ // the same problem with the descenders on the line above it!)
+ if (line > 0) {
+ top -= mLayout.getLineDescent(line - 1);
+ }
+
+ int line2;
+
+ if (first == last)
+ line2 = line;
+ else
+ line2 = mLayout.getLineForOffset(last);
+
+ int bottom = mLayout.getLineTop(line2 + 1);
+ int voffset = getVerticalOffset(true);
+
+ int left = getCompoundPaddingLeft() + mScrollX;
+ invalidate(left, top + voffset + getExtendedPaddingTop(),
+ left + getWidth() - getCompoundPaddingLeft() -
+ getCompoundPaddingRight(),
+ bottom + voffset + getExtendedPaddingTop());
+ }
+ }
+ }
+
+ private void registerForPreDraw() {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer == null) {
+ return;
+ }
+
+ if (mPreDrawState == PREDRAW_NOT_REGISTERED) {
+ observer.addOnPreDrawListener(this);
+ mPreDrawState = PREDRAW_PENDING;
+ } else if (mPreDrawState == PREDRAW_DONE) {
+ mPreDrawState = PREDRAW_PENDING;
+ }
+
+ // else state is PREDRAW_PENDING, so keep waiting.
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean onPreDraw() {
+ if (mPreDrawState != PREDRAW_PENDING) {
+ return true;
+ }
+
+ if (mLayout == null) {
+ assumeLayout();
+ }
+
+ boolean changed = false;
+
+ if (mMovement != null) {
+ int curs = Selection.getSelectionEnd(mText);
+
+ /*
+ * TODO: This should really only keep the end in view if
+ * it already was before the text changed. I'm not sure
+ * of a good way to tell from here if it was.
+ */
+ if (curs < 0 &&
+ (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ curs = mText.length();
+ }
+
+ if (curs >= 0) {
+ changed = bringPointIntoView(curs);
+ }
+ } else {
+ changed = bringTextIntoView();
+ }
+
+ mPreDrawState = PREDRAW_DONE;
+ return !changed;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mTemporaryDetach = false;
+
+ if (mShowErrorAfterAttach) {
+ showError();
+ mShowErrorAfterAttach = false;
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer != null) {
+ observer.removeOnPreDrawListener(this);
+ mPreDrawState = PREDRAW_NOT_REGISTERED;
+ }
+ }
+
+ if (mError != null) {
+ hideError();
+ }
+ }
+
+ @Override
+ protected boolean isPaddingOffsetRequired() {
+ return mShadowRadius != 0;
+ }
+
+ @Override
+ protected int getLeftPaddingOffset() {
+ return (int) Math.min(0, mShadowDx - mShadowRadius);
+ }
+
+ @Override
+ protected int getTopPaddingOffset() {
+ return (int) Math.min(0, mShadowDy - mShadowRadius);
+ }
+
+ @Override
+ protected int getBottomPaddingOffset() {
+ return (int) Math.max(0, mShadowDy + mShadowRadius);
+ }
+
+ @Override
+ protected int getRightPaddingOffset() {
+ return (int) Math.max(0, mShadowDx + mShadowRadius);
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ final boolean verified = super.verifyDrawable(who);
+ if (!verified && mDrawables != null) {
+ return who == mDrawables.mDrawableLeft || who == mDrawables.mDrawableTop ||
+ who == mDrawables.mDrawableRight || who == mDrawables.mDrawableBottom;
+ }
+ return verified;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ // Draw the background for this view
+ super.onDraw(canvas);
+
+ final int compoundPaddingLeft = getCompoundPaddingLeft();
+ final int compoundPaddingTop = getCompoundPaddingTop();
+ final int compoundPaddingRight = getCompoundPaddingRight();
+ final int compoundPaddingBottom = getCompoundPaddingBottom();
+ final int scrollX = mScrollX;
+ final int scrollY = mScrollY;
+ final int right = mRight;
+ final int left = mLeft;
+ final int bottom = mBottom;
+ final int top = mTop;
+
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ /*
+ * Compound, not extended, because the icon is not clipped
+ * if the text height is smaller.
+ */
+
+ int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
+ int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
+
+ if (dr.mDrawableLeft != null) {
+ canvas.save();
+ canvas.translate(scrollX + mPaddingLeft,
+ scrollY + compoundPaddingTop +
+ (vspace - dr.mDrawableHeightLeft) / 2);
+ dr.mDrawableLeft.draw(canvas);
+ canvas.restore();
+ }
+
+ if (dr.mDrawableRight != null) {
+ canvas.save();
+ canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight,
+ scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
+ dr.mDrawableRight.draw(canvas);
+ canvas.restore();
+ }
+
+ if (dr.mDrawableTop != null) {
+ canvas.save();
+ canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
+ scrollY + mPaddingTop);
+ dr.mDrawableTop.draw(canvas);
+ canvas.restore();
+ }
+
+ if (dr.mDrawableBottom != null) {
+ canvas.save();
+ canvas.translate(scrollX + compoundPaddingLeft +
+ (hspace - dr.mDrawableWidthBottom) / 2,
+ scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
+ dr.mDrawableBottom.draw(canvas);
+ canvas.restore();
+ }
+ }
+
+ if (mPreDrawState == PREDRAW_DONE) {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer != null) {
+ observer.removeOnPreDrawListener(this);
+ mPreDrawState = PREDRAW_NOT_REGISTERED;
+ }
+ }
+
+ int color = mCurTextColor;
+
+ if (mLayout == null) {
+ assumeLayout();
+ }
+
+ Layout layout = mLayout;
+ int cursorcolor = color;
+
+ if (mHint != null && mText.length() == 0) {
+ if (mHintTextColor != null) {
+ color = mCurHintTextColor;
+ }
+
+ layout = mHintLayout;
+ }
+
+ mTextPaint.setColor(color);
+ mTextPaint.drawableState = getDrawableState();
+
+ canvas.save();
+ /* Would be faster if we didn't have to do this. Can we chop the
+ (displayable) text so that we don't need to do this ever?
+ */
+
+ int extendedPaddingTop = getExtendedPaddingTop();
+ int extendedPaddingBottom = getExtendedPaddingBottom();
+
+ float clipLeft = compoundPaddingLeft + scrollX;
+ float clipTop = extendedPaddingTop + scrollY;
+ float clipRight = right - left - compoundPaddingRight + scrollX;
+ float clipBottom = bottom - top - extendedPaddingBottom + scrollY;
+
+ if (mShadowRadius != 0) {
+ clipLeft += Math.min(0, mShadowDx - mShadowRadius);
+ clipRight += Math.max(0, mShadowDx + mShadowRadius);
+
+ clipTop += Math.min(0, mShadowDy - mShadowRadius);
+ clipBottom += Math.max(0, mShadowDy + mShadowRadius);
+ }
+
+ canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
+
+ int voffsetText = 0;
+ int voffsetCursor = 0;
+
+ // translate in by our padding
+ {
+ /* shortcircuit calling getVerticaOffset() */
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffsetText = getVerticalOffset(false);
+ voffsetCursor = getVerticalOffset(true);
+ }
+ canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
+ }
+
+ if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
+ (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
+ canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft -
+ getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f);
+ }
+
+ if (mMarquee != null && mMarquee.isRunning()) {
+ canvas.translate(-mMarquee.mScroll, 0.0f);
+ }
+ }
+
+ Path highlight = null;
+ int selStart = -1, selEnd = -1;
+
+ // If there is no movement method, then there can be no selection.
+ // Check that first and attempt to skip everything having to do with
+ // the cursor.
+ // XXX This is not strictly true -- a program could set the
+ // selection manually if it really wanted to.
+ if (mMovement != null && (isFocused() || isPressed())) {
+ selStart = Selection.getSelectionStart(mText);
+ selEnd = Selection.getSelectionEnd(mText);
+
+ if (mCursorVisible && selStart >= 0 && isEnabled()) {
+ if (mHighlightPath == null)
+ mHighlightPath = new Path();
+
+ if (selStart == selEnd) {
+ if ((SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK)
+ < BLINK) {
+ if (mHighlightPathBogus) {
+ mHighlightPath.reset();
+ mLayout.getCursorPath(selStart, mHighlightPath, mText);
+ mHighlightPathBogus = false;
+ }
+
+ // XXX should pass to skin instead of drawing directly
+ mHighlightPaint.setColor(cursorcolor);
+ mHighlightPaint.setStyle(Paint.Style.STROKE);
+
+ highlight = mHighlightPath;
+ }
+ } else {
+ if (mHighlightPathBogus) {
+ mHighlightPath.reset();
+ mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
+ mHighlightPathBogus = false;
+ }
+
+ // XXX should pass to skin instead of drawing directly
+ mHighlightPaint.setColor(mHighlightColor);
+ mHighlightPaint.setStyle(Paint.Style.FILL);
+
+ highlight = mHighlightPath;
+ }
+ }
+ }
+
+ /* Comment out until we decide what to do about animations
+ boolean isLinearTextOn = false;
+ if (currentTransformation != null) {
+ isLinearTextOn = mTextPaint.isLinearTextOn();
+ Matrix m = currentTransformation.getMatrix();
+ if (!m.isIdentity()) {
+ // mTextPaint.setLinearTextOn(true);
+ }
+ }
+ */
+
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting == 0) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (imm.isActive(this)) {
+ boolean reported = false;
+ if (ims.mContentChanged) {
+ // We are in extract mode and the content has changed
+ // in some way... just report complete new text to the
+ // input method.
+ reported = reportExtractedText();
+ }
+ if (!reported && highlight != null) {
+ int candStart = -1;
+ int candEnd = -1;
+ if (mText instanceof Spannable) {
+ Spannable sp = (Spannable)mText;
+ candStart = EditableInputConnection.getComposingSpanStart(sp);
+ candEnd = EditableInputConnection.getComposingSpanEnd(sp);
+ }
+ imm.updateSelection(this, selStart, selEnd, candStart, candEnd);
+ }
+ }
+
+ if (imm.isWatchingCursor(this) && highlight != null) {
+ highlight.computeBounds(ims.mTmpRectF, true);
+ ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
+
+ canvas.getMatrix().mapPoints(ims.mTmpOffset);
+ ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
+
+ ims.mTmpRectF.offset(0, voffsetCursor - voffsetText);
+
+ ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
+ (int)(ims.mTmpRectF.top + 0.5),
+ (int)(ims.mTmpRectF.right + 0.5),
+ (int)(ims.mTmpRectF.bottom + 0.5));
+
+ imm.updateCursor(this,
+ ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
+ ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
+ }
+ }
+ }
+
+ layout.draw(canvas, highlight, mHighlightPaint, voffsetCursor - voffsetText);
+
+ /* Comment out until we decide what to do about animations
+ if (currentTransformation != null) {
+ mTextPaint.setLinearTextOn(isLinearTextOn);
+ }
+ */
+
+ canvas.restore();
+ }
+
+ @Override
+ public void getFocusedRect(Rect r) {
+ if (mLayout == null) {
+ super.getFocusedRect(r);
+ return;
+ }
+
+ int sel = getSelectionEnd();
+ if (sel < 0) {
+ super.getFocusedRect(r);
+ return;
+ }
+
+ int line = mLayout.getLineForOffset(sel);
+ r.top = mLayout.getLineTop(line);
+ r.bottom = mLayout.getLineBottom(line);
+
+ r.left = (int) mLayout.getPrimaryHorizontal(sel);
+ r.right = r.left + 1;
+
+ // Adjust for padding and gravity.
+ int paddingLeft = getCompoundPaddingLeft();
+ int paddingTop = getExtendedPaddingTop();
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ paddingTop += getVerticalOffset(false);
+ }
+ r.offset(paddingLeft, paddingTop);
+ }
+
+ /**
+ * Return the number of lines of text, or 0 if the internal Layout has not
+ * been built.
+ */
+ public int getLineCount() {
+ return mLayout != null ? mLayout.getLineCount() : 0;
+ }
+
+ /**
+ * Return the baseline for the specified line (0...getLineCount() - 1)
+ * If bounds is not null, return the top, left, right, bottom extents
+ * of the specified line in it. If the internal Layout has not been built,
+ * return 0 and set bounds to (0, 0, 0, 0)
+ * @param line which line to examine (0..getLineCount() - 1)
+ * @param bounds Optional. If not null, it returns the extent of the line
+ * @return the Y-coordinate of the baseline
+ */
+ public int getLineBounds(int line, Rect bounds) {
+ if (mLayout == null) {
+ if (bounds != null) {
+ bounds.set(0, 0, 0, 0);
+ }
+ return 0;
+ }
+ else {
+ int baseline = mLayout.getLineBounds(line, bounds);
+
+ int voffset = getExtendedPaddingTop();
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffset += getVerticalOffset(true);
+ }
+ if (bounds != null) {
+ bounds.offset(getCompoundPaddingLeft(), voffset);
+ }
+ return baseline + voffset;
+ }
+ }
+
+ @Override
+ public int getBaseline() {
+ if (mLayout == null) {
+ return super.getBaseline();
+ }
+
+ int voffset = 0;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ voffset = getVerticalOffset(true);
+ }
+
+ return getExtendedPaddingTop() + voffset + mLayout.getLineBaseline(0);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ int which = doKeyDown(keyCode, event, null);
+ if (which == 0) {
+ // Go through default dispatching.
+ return super.onKeyDown(keyCode, event);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+ KeyEvent down = new KeyEvent(event, KeyEvent.ACTION_DOWN);
+
+ int which = doKeyDown(keyCode, down, event);
+ if (which == 0) {
+ // Go through default dispatching.
+ return super.onKeyMultiple(keyCode, repeatCount, event);
+ }
+ if (which == -1) {
+ // Consumed the whole thing.
+ return true;
+ }
+
+ repeatCount--;
+
+ // We are going to dispatch the remaining events to either the input
+ // or movement method. To do this, we will just send a repeated stream
+ // of down and up events until we have done the complete repeatCount.
+ // It would be nice if those interfaces had an onKeyMultiple() method,
+ // but adding that is a more complicated change.
+ KeyEvent up = new KeyEvent(event, KeyEvent.ACTION_UP);
+ if (which == 1) {
+ mInput.onKeyUp(this, (Editable)mText, keyCode, up);
+ while (--repeatCount > 0) {
+ mInput.onKeyDown(this, (Editable)mText, keyCode, down);
+ mInput.onKeyUp(this, (Editable)mText, keyCode, up);
+ }
+ if (mError != null && !mErrorWasChanged) {
+ setError(null, null);
+ }
+
+ } else if (which == 2) {
+ mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
+ while (--repeatCount > 0) {
+ mMovement.onKeyDown(this, (Spannable)mText, keyCode, down);
+ mMovement.onKeyUp(this, (Spannable)mText, keyCode, up);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if pressing ENTER in this field advances focus instead
+ * of inserting the character. This is true mostly in single-line fields,
+ * but also in mail addresses and subjects which will display on multiple
+ * lines but where it doesn't make sense to insert newlines.
+ */
+ protected boolean shouldAdvanceFocusOnEnter() {
+ if (mInput == null) {
+ return false;
+ }
+
+ if (mSingleLine) {
+ return true;
+ }
+
+ if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+ int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
+
+ if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
+ variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isInterestingEnter(KeyEvent event) {
+ if ((event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0 &&
+ mInputContentType != null &&
+ (mInputContentType.imeOptions &
+ EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
+ // If this enter key came from a soft keyboard, and the
+ // text editor has been configured to not do a default
+ // action for software enter keys, then we aren't interested.
+ return false;
+ }
+ return true;
+ }
+
+ private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
+ if (!isEnabled()) {
+ return 0;
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ if (!isInterestingEnter(event)) {
+ // Ignore enter key we aren't interested in.
+ return -1;
+ }
+ if ((event.getMetaState()&KeyEvent.META_ALT_ON) == 0
+ && mInputContentType != null
+ && mInputContentType.onEditorActionListener != null) {
+ mInputContentType.enterDown = true;
+ // We are consuming the enter key for them.
+ return -1;
+ }
+ // fall through...
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (shouldAdvanceFocusOnEnter()) {
+ return 0;
+ }
+ }
+
+ if (mInput != null) {
+ /*
+ * Keep track of what the error was before doing the input
+ * so that if an input filter changed the error, we leave
+ * that error showing. Otherwise, we take down whatever
+ * error was showing when the user types something.
+ */
+ mErrorWasChanged = false;
+
+ boolean doDown = true;
+ if (otherEvent != null) {
+ try {
+ beginBatchEdit();
+ boolean handled = mInput.onKeyOther(this, (Editable) mText,
+ otherEvent);
+ if (mError != null && !mErrorWasChanged) {
+ setError(null, null);
+ }
+ doDown = false;
+ if (handled) {
+ return -1;
+ }
+ } catch (AbstractMethodError e) {
+ // onKeyOther was added after 1.0, so if it isn't
+ // implemented we need to try to dispatch as a regular down.
+ } finally {
+ endBatchEdit();
+ }
+ }
+
+ if (doDown) {
+ beginBatchEdit();
+ if (mInput.onKeyDown(this, (Editable) mText, keyCode, event)) {
+ endBatchEdit();
+ if (mError != null && !mErrorWasChanged) {
+ setError(null, null);
+ }
+ return 1;
+ }
+ endBatchEdit();
+ }
+ }
+
+ // bug 650865: sometimes we get a key event before a layout.
+ // don't try to move around if we don't know the layout.
+
+ if (mMovement != null && mLayout != null) {
+ boolean doDown = true;
+ if (otherEvent != null) {
+ try {
+ boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
+ otherEvent);
+ doDown = false;
+ if (handled) {
+ return -1;
+ }
+ } catch (AbstractMethodError e) {
+ // onKeyOther was added after 1.0, so if it isn't
+ // implemented we need to try to dispatch as a regular down.
+ }
+ }
+ if (doDown) {
+ if (mMovement.onKeyDown(this, (Spannable)mText, keyCode, event))
+ return 2;
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (!isEnabled()) {
+ return super.onKeyUp(keyCode, event);
+ }
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ /*
+ * If there is a click listener, just call through to
+ * super, which will invoke it.
+ *
+ * If there isn't a click listener, try to show the soft
+ * input method. (It will also
+ * call performClick(), but that won't do anything in
+ * this case.)
+ */
+ if (mOnClickListener == null) {
+ if (mMovement != null && mText instanceof Editable
+ && mLayout != null && onCheckIsTextEditor()) {
+ InputMethodManager imm = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(this, 0);
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+
+ case KeyEvent.KEYCODE_ENTER:
+ if (mInputContentType != null
+ && mInputContentType.onEditorActionListener != null
+ && mInputContentType.enterDown) {
+ mInputContentType.enterDown = false;
+ if (mInputContentType.onEditorActionListener.onEditorAction(
+ this, EditorInfo.IME_UNDEFINED, event)) {
+ return true;
+ }
+ }
+
+ if (shouldAdvanceFocusOnEnter()) {
+ /*
+ * If there is a click listener, just call through to
+ * super, which will invoke it.
+ *
+ * If there isn't a click listener, try to advance focus,
+ * but still call through to super, which will reset the
+ * pressed state and longpress state. (It will also
+ * call performClick(), but that won't do anything in
+ * this case.)
+ */
+ if (mOnClickListener == null) {
+ View v = focusSearch(FOCUS_DOWN);
+
+ if (v != null) {
+ if (!v.requestFocus(FOCUS_DOWN)) {
+ throw new IllegalStateException("focus search returned a view " +
+ "that wasn't able to take focus!");
+ }
+
+ /*
+ * Return true because we handled the key; super
+ * will return false because there was no click
+ * listener.
+ */
+ super.onKeyUp(keyCode, event);
+ return true;
+ }
+ }
+
+ return super.onKeyUp(keyCode, event);
+ }
+ }
+
+ if (mInput != null)
+ if (mInput.onKeyUp(this, (Editable) mText, keyCode, event))
+ return true;
+
+ if (mMovement != null && mLayout != null)
+ if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event))
+ return true;
+
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override public boolean onCheckIsTextEditor() {
+ return mInputType != EditorInfo.TYPE_NULL;
+ }
+
+ @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+ if (onCheckIsTextEditor()) {
+ if (mInputMethodState == null) {
+ mInputMethodState = new InputMethodState();
+ }
+ outAttrs.inputType = mInputType;
+ if (mInputContentType != null) {
+ outAttrs.imeOptions = mInputContentType.imeOptions;
+ outAttrs.privateImeOptions = mInputContentType.privateImeOptions;
+ outAttrs.actionLabel = mInputContentType.imeActionLabel;
+ outAttrs.actionId = mInputContentType.imeActionId;
+ outAttrs.extras = mInputContentType.extras;
+ } else {
+ outAttrs.imeOptions = EditorInfo.IME_UNDEFINED;
+ }
+ if (outAttrs.imeOptions == EditorInfo.IME_UNDEFINED) {
+ if (focusSearch(FOCUS_DOWN) != null) {
+ // An action has not been set, but the enter key will move to
+ // the next focus, so set the action to that.
+ outAttrs.imeOptions = EditorInfo.IME_ACTION_NEXT;
+ if (!shouldAdvanceFocusOnEnter()) {
+ outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
+ }
+ }
+ }
+ outAttrs.hintText = mHint;
+ if (mText instanceof Editable) {
+ InputConnection ic = new EditableInputConnection(this);
+ outAttrs.initialSelStart = Selection.getSelectionStart(mText);
+ outAttrs.initialSelEnd = Selection.getSelectionEnd(mText);
+ outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
+ return ic;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * If this TextView contains editable content, extract a portion of it
+ * based on the information in <var>request</var> in to <var>outText</var>.
+ * @return Returns true if the text was successfully extracted, else false.
+ */
+ public boolean extractText(ExtractedTextRequest request,
+ ExtractedText outText) {
+ return extractTextInternal(request, -1, -1, -1, outText);
+ }
+
+ boolean extractTextInternal(ExtractedTextRequest request,
+ int partialStartOffset, int partialEndOffset, int delta,
+ ExtractedText outText) {
+ final CharSequence content = mText;
+ if (content != null) {
+ final int N = content.length();
+ if (partialStartOffset < 0) {
+ outText.partialStartOffset = outText.partialEndOffset = -1;
+ partialStartOffset = 0;
+ partialEndOffset = N;
+ } else {
+ // Adjust offsets to ensure we contain full spans.
+ if (content instanceof Spanned) {
+ Spanned spanned = (Spanned)content;
+ Object[] spans = spanned.getSpans(partialStartOffset,
+ partialEndOffset, ParcelableSpan.class);
+ int i = spans.length;
+ while (i > 0) {
+ i--;
+ int j = spanned.getSpanStart(spans[i]);
+ if (j < partialStartOffset) partialStartOffset = j;
+ j = spanned.getSpanEnd(spans[i]);
+ if (j > partialEndOffset) partialEndOffset = j;
+ }
+ }
+ outText.partialStartOffset = partialStartOffset;
+ outText.partialEndOffset = partialEndOffset;
+ // Now use the delta to determine the actual amount of text
+ // we need.
+ partialEndOffset += delta;
+ if (partialEndOffset > N) {
+ partialEndOffset = N;
+ } else if (partialEndOffset < 0) {
+ partialEndOffset = 0;
+ }
+ }
+ if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
+ outText.text = content.subSequence(partialStartOffset,
+ partialEndOffset);
+ } else {
+ outText.text = TextUtils.substring(content, partialStartOffset,
+ partialEndOffset);
+ }
+ outText.startOffset = 0;
+ outText.selectionStart = Selection.getSelectionStart(content);
+ outText.selectionEnd = Selection.getSelectionEnd(content);
+ return true;
+ }
+ return false;
+ }
+
+ boolean reportExtractedText() {
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mContentChanged) {
+ ims.mContentChanged = false;
+ final ExtractedTextRequest req = mInputMethodState.mExtracting;
+ if (req != null) {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "Retrieving extracted start="
+ + ims.mChangedStart + " end=" + ims.mChangedEnd
+ + " delta=" + ims.mChangedDelta);
+ if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
+ ims.mChangedDelta, ims.mTmpExtracted)) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "Reporting extracted start="
+ + ims.mTmpExtracted.partialStartOffset
+ + " end=" + ims.mTmpExtracted.partialEndOffset
+ + ": " + ims.mTmpExtracted.text);
+ imm.updateExtractedText(this, req.token,
+ mInputMethodState.mTmpExtracted);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This is used to remove all style-impacting spans from text before new
+ * extracted text is being replaced into it, so that we don't have any
+ * lingering spans applied during the replace.
+ */
+ static void removeParcelableSpans(Spannable spannable, int start, int end) {
+ Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
+ int i = spans.length;
+ while (i > 0) {
+ i--;
+ spannable.removeSpan(spans[i]);
+ }
+ }
+
+ /**
+ * Apply to this text view the given extracted text, as previously
+ * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
+ */
+ public void setExtractedText(ExtractedText text) {
+ Editable content = getEditableText();
+ if (content == null) {
+ setText(text.text, TextView.BufferType.EDITABLE);
+ } else if (text.partialStartOffset < 0) {
+ removeParcelableSpans(content, 0, content.length());
+ content.replace(0, content.length(), text.text);
+ } else {
+ final int N = content.length();
+ int start = text.partialStartOffset;
+ if (start > N) start = N;
+ int end = text.partialEndOffset;
+ if (end > N) end = N;
+ removeParcelableSpans(content, start, end);
+ content.replace(start, end, text.text);
+ }
+
+ // Now set the selection position... make sure it is in range, to
+ // avoid crashes. If this is a partial update, it is possible that
+ // the underlying text may have changed, causing us problems here.
+ // Also we just don't want to trust clients to do the right thing.
+ Spannable sp = (Spannable)getText();
+ final int N = sp.length();
+ int start = text.selectionStart;
+ if (start < 0) start = 0;
+ else if (start > N) start = N;
+ int end = text.selectionEnd;
+ if (end < 0) end = 0;
+ else if (end > N) end = N;
+ Selection.setSelection(sp, start, end);
+ }
+
+ /**
+ * @hide
+ */
+ public void setExtracting(ExtractedTextRequest req) {
+ if (mInputMethodState != null) {
+ mInputMethodState.mExtracting = req;
+ }
+ }
+
+ /**
+ * Called by the framework in response to a text completion from
+ * the current input method, provided by it calling
+ * {@link InputConnection#commitCompletion
+ * InputConnection.commitCompletion()}. The default implementation does
+ * nothing; text views that are supporting auto-completion should override
+ * this to do their desired behavior.
+ *
+ * @param text The auto complete text the user has selected.
+ */
+ public void onCommitCompletion(CompletionInfo text) {
+ }
+
+ public void beginBatchEdit() {
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = ++ims.mBatchEditNesting;
+ if (nesting == 1) {
+ ims.mCursorChanged = false;
+ ims.mChangedDelta = 0;
+ if (ims.mContentChanged) {
+ // We already have a pending change from somewhere else,
+ // so turn this into a full update.
+ ims.mChangedStart = 0;
+ ims.mChangedEnd = mText.length();
+ } else {
+ ims.mChangedStart = -1;
+ ims.mChangedEnd = -1;
+ ims.mContentChanged = false;
+ }
+ onBeginBatchEdit();
+ }
+ }
+ }
+
+ public void endBatchEdit() {
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null) {
+ int nesting = --ims.mBatchEditNesting;
+ if (nesting == 0) {
+ finishBatchEdit(ims);
+ }
+ }
+ }
+
+ void ensureEndedBatchEdit() {
+ final InputMethodState ims = mInputMethodState;
+ if (ims != null && ims.mBatchEditNesting != 0) {
+ ims.mBatchEditNesting = 0;
+ finishBatchEdit(ims);
+ }
+ }
+
+ void finishBatchEdit(final InputMethodState ims) {
+ onEndBatchEdit();
+
+ if (ims.mContentChanged) {
+ updateAfterEdit();
+ reportExtractedText();
+ } else if (ims.mCursorChanged) {
+ // Cheezy way to get us to report the current cursor location.
+ invalidateCursor();
+ }
+ }
+
+ void updateAfterEdit() {
+ invalidate();
+ int curs = Selection.getSelectionStart(mText);
+
+ if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) ==
+ Gravity.BOTTOM) {
+ registerForPreDraw();
+ }
+
+ if (curs >= 0) {
+ mHighlightPathBogus = true;
+
+ if (isFocused()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ makeBlink();
+ }
+ }
+
+ checkForResize();
+ }
+
+ /**
+ * Called by the framework in response to a request to begin a batch
+ * of edit operations through a call to link {@link #beginBatchEdit()}.
+ */
+ public void onBeginBatchEdit() {
+ }
+
+ /**
+ * Called by the framework in response to a request to end a batch
+ * of edit operations through a call to link {@link #endBatchEdit}.
+ */
+ public void onEndBatchEdit() {
+ }
+
+ /**
+ * Called by the framework in response to a private command from the
+ * current method, provided by it calling
+ * {@link InputConnection#performPrivateCommand
+ * InputConnection.performPrivateCommand()}.
+ *
+ * @param action The action name of the command.
+ * @param data Any additional data for the command. This may be null.
+ * @return Return true if you handled the command, else false.
+ */
+ public boolean onPrivateIMECommand(String action, Bundle data) {
+ return false;
+ }
+
+ private void nullLayouts() {
+ if (mLayout instanceof BoringLayout && mSavedLayout == null) {
+ mSavedLayout = (BoringLayout) mLayout;
+ }
+ if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
+ mSavedHintLayout = (BoringLayout) mHintLayout;
+ }
+
+ mLayout = mHintLayout = null;
+ }
+
+ /**
+ * Make a new Layout based on the already-measured size of the view,
+ * on the assumption that it was measured correctly at some point.
+ */
+ private void assumeLayout() {
+ int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+
+ if (width < 1) {
+ width = 0;
+ }
+
+ int physicalWidth = width;
+
+ if (mHorizontallyScrolling) {
+ width = VERY_WIDE;
+ }
+
+ makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
+ physicalWidth, false);
+ }
+
+ /**
+ * The width passed in is now the desired layout width,
+ * not the full view width with padding.
+ * {@hide}
+ */
+ protected void makeNewLayout(int w, int hintWidth,
+ BoringLayout.Metrics boring,
+ BoringLayout.Metrics hintBoring,
+ int ellipsisWidth, boolean bringIntoView) {
+ stopMarquee();
+
+ mHighlightPathBogus = true;
+
+ if (w < 0) {
+ w = 0;
+ }
+ if (hintWidth < 0) {
+ hintWidth = 0;
+ }
+
+ Layout.Alignment alignment;
+ switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ alignment = Layout.Alignment.ALIGN_CENTER;
+ break;
+
+ case Gravity.RIGHT:
+ alignment = Layout.Alignment.ALIGN_OPPOSITE;
+ break;
+
+ default:
+ alignment = Layout.Alignment.ALIGN_NORMAL;
+ }
+
+ if (mText instanceof Spannable) {
+ mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w,
+ alignment, mSpacingMult,
+ mSpacingAdd, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ if (boring == UNKNOWN_BORING) {
+ boring = BoringLayout.isBoring(mTransformed, mTextPaint,
+ mBoring);
+ if (boring != null) {
+ mBoring = boring;
+ }
+ }
+
+ if (boring != null) {
+ if (boring.width <= w &&
+ (mEllipsize == null || boring.width <= ellipsisWidth)) {
+ if (mSavedLayout != null) {
+ mLayout = mSavedLayout.
+ replaceOrMake(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad);
+ } else {
+ mLayout = BoringLayout.make(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad);
+ }
+ // Log.e("aaa", "Boring: " + mTransformed);
+
+ mSavedLayout = (BoringLayout) mLayout;
+ } else if (mEllipsize != null && boring.width <= w) {
+ if (mSavedLayout != null) {
+ mLayout = mSavedLayout.
+ replaceOrMake(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ mLayout = BoringLayout.make(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ boring, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ }
+ } else if (mEllipsize != null) {
+ mLayout = new StaticLayout(mTransformed,
+ 0, mTransformed.length(),
+ mTextPaint, w, alignment, mSpacingMult,
+ mSpacingAdd, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ mLayout = new StaticLayout(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ // Log.e("aaa", "Boring but wide: " + mTransformed);
+ }
+ } else if (mEllipsize != null) {
+ mLayout = new StaticLayout(mTransformed,
+ 0, mTransformed.length(),
+ mTextPaint, w, alignment, mSpacingMult,
+ mSpacingAdd, mIncludePad, mEllipsize,
+ ellipsisWidth);
+ } else {
+ mLayout = new StaticLayout(mTransformed, mTextPaint,
+ w, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ }
+ }
+
+ mHintLayout = null;
+
+ if (mHint != null) {
+ if (hintBoring == UNKNOWN_BORING) {
+ hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
+ mHintBoring);
+ if (hintBoring != null) {
+ mHintBoring = hintBoring;
+ }
+ }
+
+ if (hintBoring != null) {
+ if (hintBoring.width <= hintWidth) {
+ if (mSavedHintLayout != null) {
+ mHintLayout = mSavedHintLayout.
+ replaceOrMake(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult,
+ mSpacingAdd, hintBoring, mIncludePad);
+ } else {
+ mHintLayout = BoringLayout.make(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult,
+ mSpacingAdd, hintBoring, mIncludePad);
+ }
+
+ mSavedHintLayout = (BoringLayout) mHintLayout;
+ } else {
+ mHintLayout = new StaticLayout(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ }
+ } else {
+ mHintLayout = new StaticLayout(mHint, mTextPaint,
+ hintWidth, alignment, mSpacingMult, mSpacingAdd,
+ mIncludePad);
+ }
+ }
+
+ if (bringIntoView) {
+ registerForPreDraw();
+ }
+
+ if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ final int height = mLayoutParams.height;
+ // If the size of the view does not depend on the size of the text, try to
+ // start the marquee immediately
+ if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.FILL_PARENT) {
+ startMarquee();
+ } else {
+ // Defer the start of the marquee until we know our width (see setFrame())
+ mRestartMarquee = true;
+ }
+ }
+ }
+
+ private static int desired(Layout layout) {
+ int n = layout.getLineCount();
+ CharSequence text = layout.getText();
+ float max = 0;
+
+ // if any line was wrapped, we can't use it.
+ // but it's ok for the last line not to have a newline
+
+ for (int i = 0; i < n - 1; i++) {
+ if (text.charAt(layout.getLineEnd(i) - 1) != '\n')
+ return -1;
+ }
+
+ for (int i = 0; i < n; i++) {
+ max = Math.max(max, layout.getLineWidth(i));
+ }
+
+ return (int) FloatMath.ceil(max);
+ }
+
+ /**
+ * Set whether the TextView includes extra top and bottom padding to make
+ * room for accents that go above the normal ascent and descent.
+ * The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ */
+ public void setIncludeFontPadding(boolean includepad) {
+ mIncludePad = includepad;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ private static final BoringLayout.Metrics UNKNOWN_BORING =
+ new BoringLayout.Metrics();
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ int width;
+ int height;
+
+ BoringLayout.Metrics boring = UNKNOWN_BORING;
+ BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
+
+ int des = -1;
+ boolean fromexisting = false;
+
+ if (widthMode == MeasureSpec.EXACTLY) {
+ // Parent has told us how big to be. So be it.
+ width = widthSize;
+ } else {
+ if (mLayout != null && mEllipsize == null) {
+ des = desired(mLayout);
+ }
+
+ if (des < 0) {
+ boring = BoringLayout.isBoring(mTransformed, mTextPaint,
+ mBoring);
+ if (boring != null) {
+ mBoring = boring;
+ }
+ } else {
+ fromexisting = true;
+ }
+
+ if (boring == null || boring == UNKNOWN_BORING) {
+ if (des < 0) {
+ des = (int) FloatMath.ceil(Layout.
+ getDesiredWidth(mTransformed, mTextPaint));
+ }
+
+ width = des;
+ } else {
+ width = boring.width;
+ }
+
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ width = Math.max(width, dr.mDrawableWidthTop);
+ width = Math.max(width, dr.mDrawableWidthBottom);
+ }
+
+ if (mHint != null) {
+ int hintDes = -1;
+ int hintWidth;
+
+ if (mHintLayout != null) {
+ hintDes = desired(mHintLayout);
+ }
+
+ if (hintDes < 0) {
+ hintBoring = BoringLayout.isBoring(mHint, mTextPaint,
+ mHintBoring);
+ if (hintBoring != null) {
+ mHintBoring = hintBoring;
+ }
+ }
+
+ if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
+ if (hintDes < 0) {
+ hintDes = (int) FloatMath.ceil(Layout.
+ getDesiredWidth(mHint, mTextPaint));
+ }
+
+ hintWidth = hintDes;
+ } else {
+ hintWidth = hintBoring.width;
+ }
+
+ if (hintWidth > width) {
+ width = hintWidth;
+ }
+ }
+
+ width += getCompoundPaddingLeft() + getCompoundPaddingRight();
+
+ if (mMaxWidthMode == EMS) {
+ width = Math.min(width, mMaxWidth * getLineHeight());
+ } else {
+ width = Math.min(width, mMaxWidth);
+ }
+
+ if (mMinWidthMode == EMS) {
+ width = Math.max(width, mMinWidth * getLineHeight());
+ } else {
+ width = Math.max(width, mMinWidth);
+ }
+
+ // Check against our minimum width
+ width = Math.max(width, getSuggestedMinimumWidth());
+
+ if (widthMode == MeasureSpec.AT_MOST) {
+ width = Math.min(widthSize, width);
+ }
+ }
+
+ int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int unpaddedWidth = want;
+ int hintWant = want;
+
+ if (mHorizontallyScrolling)
+ want = VERY_WIDE;
+
+ int hintWidth = mHintLayout == null ? hintWant : mHintLayout.getWidth();
+
+ if (mLayout == null) {
+ makeNewLayout(want, hintWant, boring, hintBoring,
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+ false);
+ } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) ||
+ (mLayout.getEllipsizedWidth() !=
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
+ if (mHint == null && mEllipsize == null &&
+ want > mLayout.getWidth() &&
+ (mLayout instanceof BoringLayout ||
+ (fromexisting && des >= 0 && des <= want))) {
+ mLayout.increaseWidthTo(want);
+ } else {
+ makeNewLayout(want, hintWant, boring, hintBoring,
+ width - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+ false);
+ }
+ } else {
+ // Width has not changed.
+ }
+
+ if (heightMode == MeasureSpec.EXACTLY) {
+ // Parent has told us how big to be. So be it.
+ height = heightSize;
+ mDesiredHeightAtMeasure = -1;
+ } else {
+ int desired = getDesiredHeight();
+
+ height = desired;
+ mDesiredHeightAtMeasure = desired;
+
+ if (heightMode == MeasureSpec.AT_MOST) {
+ height = Math.min(desired, height);
+ }
+ }
+
+ int unpaddedHeight = height - getCompoundPaddingTop() -
+ getCompoundPaddingBottom();
+ if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
+ unpaddedHeight = Math.min(unpaddedHeight,
+ mLayout.getLineTop(mMaximum));
+ }
+
+ /*
+ * We didn't let makeNewLayout() register to bring the cursor into view,
+ * so do it here if there is any possibility that it is needed.
+ */
+ if (mMovement != null ||
+ mLayout.getWidth() > unpaddedWidth ||
+ mLayout.getHeight() > unpaddedHeight) {
+ registerForPreDraw();
+ } else {
+ scrollTo(0, 0);
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ private int getDesiredHeight() {
+ return Math.max(getDesiredHeight(mLayout, true),
+ getDesiredHeight(mHintLayout, false));
+ }
+
+ private int getDesiredHeight(Layout layout, boolean cap) {
+ if (layout == null) {
+ return 0;
+ }
+
+ int linecount = layout.getLineCount();
+ int pad = getCompoundPaddingTop() + getCompoundPaddingBottom();
+ int desired = layout.getLineTop(linecount);
+
+ final Drawables dr = mDrawables;
+ if (dr != null) {
+ desired = Math.max(desired, dr.mDrawableHeightLeft);
+ desired = Math.max(desired, dr.mDrawableHeightRight);
+ }
+
+ desired += pad;
+
+ if (mMaxMode == LINES) {
+ /*
+ * Don't cap the hint to a certain number of lines.
+ * (Do cap it, though, if we have a maximum pixel height.)
+ */
+ if (cap) {
+ if (linecount > mMaximum) {
+ desired = layout.getLineTop(mMaximum) +
+ layout.getBottomPadding();
+
+ if (dr != null) {
+ desired = Math.max(desired, dr.mDrawableHeightLeft);
+ desired = Math.max(desired, dr.mDrawableHeightRight);
+ }
+
+ desired += pad;
+ linecount = mMaximum;
+ }
+ }
+ } else {
+ desired = Math.min(desired, mMaximum);
+ }
+
+ if (mMinMode == LINES) {
+ if (linecount < mMinimum) {
+ desired += getLineHeight() * (mMinimum - linecount);
+ }
+ } else {
+ desired = Math.max(desired, mMinimum);
+ }
+
+ // Check against our minimum height
+ desired = Math.max(desired, getSuggestedMinimumHeight());
+
+ return desired;
+ }
+
+ /**
+ * Check whether a change to the existing text layout requires a
+ * new view layout.
+ */
+ private void checkForResize() {
+ boolean sizeChanged = false;
+
+ if (mLayout != null) {
+ // Check if our width changed
+ if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
+ sizeChanged = true;
+ invalidate();
+ }
+
+ // Check if our height changed
+ if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
+ int desiredHeight = getDesiredHeight();
+
+ if (desiredHeight != this.getHeight()) {
+ sizeChanged = true;
+ }
+ } else if (mLayoutParams.height == LayoutParams.FILL_PARENT) {
+ if (mDesiredHeightAtMeasure >= 0) {
+ int desiredHeight = getDesiredHeight();
+
+ if (desiredHeight != mDesiredHeightAtMeasure) {
+ sizeChanged = true;
+ }
+ }
+ }
+ }
+
+ if (sizeChanged) {
+ requestLayout();
+ // caller will have already invalidated
+ }
+ }
+
+ /**
+ * Check whether entirely new text requires a new view layout
+ * or merely a new text layout.
+ */
+ private void checkForRelayout() {
+ // If we have a fixed width, we can just swap in a new text layout
+ // if the text height stays the same or if the view height is fixed.
+
+ if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT ||
+ (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth)) &&
+ (mHint == null || mHintLayout != null) &&
+ (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
+ // Static width, so try making a new text layout.
+
+ int oldht = mLayout.getHeight();
+ int want = mLayout.getWidth();
+ int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
+
+ /*
+ * No need to bring the text into view, since the size is not
+ * changing (unless we do the requestLayout(), in which case it
+ * will happen at measure).
+ */
+ makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
+ mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
+
+ // In a fixed-height view, so use our new text layout.
+ if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
+ mLayoutParams.height != LayoutParams.FILL_PARENT) {
+ invalidate();
+ return;
+ }
+
+ // Dynamic height, but height has stayed the same,
+ // so use our new text layout.
+ if (mLayout.getHeight() == oldht &&
+ (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
+ invalidate();
+ return;
+ }
+
+ // We lose: the height has changed and we have a dynamic height.
+ // Request a new view layout using our new text layout.
+ requestLayout();
+ invalidate();
+ } else {
+ // Dynamic width, so we have no choice but to request a new
+ // view layout with a new text layout.
+
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Returns true if anything changed.
+ */
+ private boolean bringTextIntoView() {
+ int line = 0;
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ line = mLayout.getLineCount() - 1;
+ }
+
+ Layout.Alignment a = mLayout.getParagraphAlignment(line);
+ int dir = mLayout.getParagraphDirection(line);
+ int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+ int ht = mLayout.getHeight();
+
+ int scrollx, scrolly;
+
+ if (a == Layout.Alignment.ALIGN_CENTER) {
+ /*
+ * Keep centered if possible, or, if it is too wide to fit,
+ * keep leading edge in view.
+ */
+
+ int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+
+ if (right - left < hspace) {
+ scrollx = (right + left) / 2 - hspace / 2;
+ } else {
+ if (dir < 0) {
+ scrollx = right - hspace;
+ } else {
+ scrollx = left;
+ }
+ }
+ } else if (a == Layout.Alignment.ALIGN_NORMAL) {
+ /*
+ * Keep leading edge in view.
+ */
+
+ if (dir < 0) {
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+ scrollx = right - hspace;
+ } else {
+ scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ }
+ } else /* a == Layout.Alignment.ALIGN_OPPOSITE */ {
+ /*
+ * Keep trailing edge in view.
+ */
+
+ if (dir < 0) {
+ scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ } else {
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+ scrollx = right - hspace;
+ }
+ }
+
+ if (ht < vspace) {
+ scrolly = 0;
+ } else {
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+ scrolly = ht - vspace;
+ } else {
+ scrolly = 0;
+ }
+ }
+
+ if (scrollx != mScrollX || scrolly != mScrollY) {
+ scrollTo(scrollx, scrolly);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Move the point, specified by the offset, into the view if it is needed.
+ * This has to be called after layout. Returns true if anything changed.
+ */
+ public boolean bringPointIntoView(int offset) {
+ boolean changed = false;
+
+ int line = mLayout.getLineForOffset(offset);
+
+ // FIXME: Is it okay to truncate this, or should we round?
+ final int x = (int)mLayout.getPrimaryHorizontal(offset);
+ final int top = mLayout.getLineTop(line);
+ final int bottom = mLayout.getLineTop(line+1);
+
+ int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
+ int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
+ int ht = mLayout.getHeight();
+
+ int grav;
+
+ switch (mLayout.getParagraphAlignment(line)) {
+ case ALIGN_NORMAL:
+ grav = 1;
+ break;
+
+ case ALIGN_OPPOSITE:
+ grav = -1;
+ break;
+
+ default:
+ grav = 0;
+ }
+
+ grav *= mLayout.getParagraphDirection(line);
+
+ int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+ int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+
+ int hslack = (bottom - top) / 2;
+ int vslack = hslack;
+
+ if (vslack > vspace / 4)
+ vslack = vspace / 4;
+ if (hslack > hspace / 4)
+ hslack = hspace / 4;
+
+ int hs = mScrollX;
+ int vs = mScrollY;
+
+ if (top - vs < vslack)
+ vs = top - vslack;
+ if (bottom - vs > vspace - vslack)
+ vs = bottom - (vspace - vslack);
+ if (ht - vs < vspace)
+ vs = ht - vspace;
+ if (0 - vs > 0)
+ vs = 0;
+
+ if (grav != 0) {
+ if (x - hs < hslack) {
+ hs = x - hslack;
+ }
+ if (x - hs > hspace - hslack) {
+ hs = x - (hspace - hslack);
+ }
+ }
+
+ if (grav < 0) {
+ if (left - hs > 0)
+ hs = left;
+ if (right - hs < hspace)
+ hs = right - hspace;
+ } else if (grav > 0) {
+ if (right - hs < hspace)
+ hs = right - hspace;
+ if (left - hs > 0)
+ hs = left;
+ } else /* grav == 0 */ {
+ if (right - left <= hspace) {
+ /*
+ * If the entire text fits, center it exactly.
+ */
+ hs = left - (hspace - (right - left)) / 2;
+ } else if (x > right - hslack) {
+ /*
+ * If we are near the right edge, keep the right edge
+ * at the edge of the view.
+ */
+ hs = right - hspace;
+ } else if (x < left + hslack) {
+ /*
+ * If we are near the left edge, keep the left edge
+ * at the edge of the view.
+ */
+ hs = left;
+ } else if (left > hs) {
+ /*
+ * Is there whitespace visible at the left? Fix it if so.
+ */
+ hs = left;
+ } else if (right < hs + hspace) {
+ /*
+ * Is there whitespace visible at the right? Fix it if so.
+ */
+ hs = right - hspace;
+ } else {
+ /*
+ * Otherwise, float as needed.
+ */
+ if (x - hs < hslack) {
+ hs = x - hslack;
+ }
+ if (x - hs > hspace - hslack) {
+ hs = x - (hspace - hslack);
+ }
+ }
+ }
+
+ if (hs != mScrollX || vs != mScrollY) {
+ if (mScroller == null) {
+ scrollTo(hs, vs);
+ } else {
+ long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+ int dx = hs - mScrollX;
+ int dy = vs - mScrollY;
+
+ if (duration > ANIMATED_SCROLL_GAP) {
+ mScroller.startScroll(mScrollX, mScrollY, dx, dy);
+ invalidate();
+ } else {
+ if (!mScroller.isFinished()) {
+ mScroller.abortAnimation();
+ }
+
+ scrollBy(dx, dy);
+ }
+
+ mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+ }
+
+ changed = true;
+ }
+
+ if (isFocused()) {
+ // This offsets because getInterestingRect() is in terms of
+ // viewport coordinates, but requestRectangleOnScreen()
+ // is in terms of content coordinates.
+
+ Rect r = new Rect();
+ getInterestingRect(r, x, top, bottom, line);
+ r.offset(mScrollX, mScrollY);
+
+ if (requestRectangleOnScreen(r)) {
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller != null) {
+ if (mScroller.computeScrollOffset()) {
+ mScrollX = mScroller.getCurrX();
+ mScrollY = mScroller.getCurrY();
+ postInvalidate(); // So we draw again
+ }
+ }
+ }
+
+ private void getInterestingRect(Rect r, int h, int top, int bottom,
+ int line) {
+ int paddingTop = getExtendedPaddingTop();
+ if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+ paddingTop += getVerticalOffset(false);
+ }
+ top += paddingTop;
+ bottom += paddingTop;
+ h += getCompoundPaddingLeft();
+
+ if (line == 0)
+ top -= getExtendedPaddingTop();
+ if (line == mLayout.getLineCount() - 1)
+ bottom += getExtendedPaddingBottom();
+
+ r.set(h, top, h+1, bottom);
+ r.offset(-mScrollX, -mScrollY);
+ }
+
+ @Override
+ public void debug(int depth) {
+ super.debug(depth);
+
+ String output = debugIndent(depth);
+ output += "frame={" + mLeft + ", " + mTop + ", " + mRight
+ + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
+ + "} ";
+
+ if (mText != null) {
+
+ output += "mText=\"" + mText + "\" ";
+ if (mLayout != null) {
+ output += "mLayout width=" + mLayout.getWidth()
+ + " height=" + mLayout.getHeight();
+ }
+ } else {
+ output += "mText=NULL";
+ }
+ Log.d(VIEW_LOG_TAG, output);
+ }
+
+ /**
+ * Convenience for {@link Selection#getSelectionStart}.
+ */
+ public int getSelectionStart() {
+ return Selection.getSelectionStart(getText());
+ }
+
+ /**
+ * Convenience for {@link Selection#getSelectionEnd}.
+ */
+ public int getSelectionEnd() {
+ return Selection.getSelectionEnd(getText());
+ }
+
+ /**
+ * Return true iff there is a selection inside this text view.
+ */
+ public boolean hasSelection() {
+ return getSelectionStart() != getSelectionEnd();
+ }
+
+ /**
+ * Sets the properties of this field (lines, horizontally scrolling,
+ * transformation method) to be for a single-line input.
+ *
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ public void setSingleLine() {
+ setSingleLine(true);
+ }
+
+ /**
+ * If true, sets the properties of this field (lines, horizontally
+ * scrolling, transformation method) to be for a single-line input;
+ * if false, restores these to the default conditions.
+ * Note that calling this with false restores default conditions,
+ * not necessarily those that were in effect prior to calling
+ * it with true.
+ *
+ * @attr ref android.R.styleable#TextView_singleLine
+ */
+ @android.view.RemotableViewMethod
+ public void setSingleLine(boolean singleLine) {
+ if ((mInputType&EditorInfo.TYPE_MASK_CLASS)
+ == EditorInfo.TYPE_CLASS_TEXT) {
+ if (singleLine) {
+ mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ } else {
+ mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+ }
+ applySingleLine(singleLine, true);
+ }
+
+ private void applySingleLine(boolean singleLine, boolean applyTransformation) {
+ mSingleLine = singleLine;
+ if (singleLine) {
+ setLines(1);
+ setHorizontallyScrolling(true);
+ if (applyTransformation) {
+ setTransformationMethod(SingleLineTransformationMethod.
+ getInstance());
+ }
+ } else {
+ setMaxLines(Integer.MAX_VALUE);
+ setHorizontallyScrolling(false);
+ if (applyTransformation) {
+ setTransformationMethod(null);
+ }
+ }
+ }
+
+ /**
+ * Causes words in the text that are longer than the view is wide
+ * to be ellipsized instead of broken in the middle. You may also
+ * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
+ * to constrain the text toa single line. Use <code>null</code>
+ * to turn off ellipsizing.
+ *
+ * @attr ref android.R.styleable#TextView_ellipsize
+ */
+ public void setEllipsize(TextUtils.TruncateAt where) {
+ mEllipsize = where;
+
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+
+ /**
+ * Sets how many times to repeat the marquee animation. Only applied if the
+ * TextView has marquee enabled. Set to -1 to repeat indefinitely.
+ *
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ */
+ public void setMarqueeRepeatLimit(int marqueeLimit) {
+ mMarqueeRepeatLimit = marqueeLimit;
+ }
+
+ /**
+ * Returns where, if anywhere, words that are longer than the view
+ * is wide should be ellipsized.
+ */
+ @ViewDebug.ExportedProperty
+ public TextUtils.TruncateAt getEllipsize() {
+ return mEllipsize;
+ }
+
+ /**
+ * Set the TextView so that when it takes focus, all the text is
+ * selected.
+ *
+ * @attr ref android.R.styleable#TextView_selectAllOnFocus
+ */
+ @android.view.RemotableViewMethod
+ public void setSelectAllOnFocus(boolean selectAllOnFocus) {
+ mSelectAllOnFocus = selectAllOnFocus;
+
+ if (selectAllOnFocus && !(mText instanceof Spannable)) {
+ setText(mText, BufferType.SPANNABLE);
+ }
+ }
+
+ /**
+ * Set whether the cursor is visible. The default is true.
+ *
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ */
+ @android.view.RemotableViewMethod
+ public void setCursorVisible(boolean visible) {
+ mCursorVisible = visible;
+ invalidate();
+
+ if (visible) {
+ makeBlink();
+ } else if (mBlink != null) {
+ mBlink.removeCallbacks(mBlink);
+ }
+ }
+
+ private boolean canMarquee() {
+ int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
+ return width > 0 && mLayout.getLineWidth(0) > width;
+ }
+
+ private void startMarquee() {
+ if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
+ getLineCount() == 1 && canMarquee()) {
+ if (mMarquee == null) mMarquee = new Marquee(this);
+ mMarquee.start(mMarqueeRepeatLimit);
+ }
+ }
+
+ private void stopMarquee() {
+ if (mMarquee != null && !mMarquee.isStopped()) {
+ mMarquee.stop();
+ }
+ }
+
+ private void startStopMarquee(boolean start) {
+ if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (start) {
+ startMarquee();
+ } else {
+ stopMarquee();
+ }
+ }
+ }
+
+ private static final class Marquee extends Handler {
+ // TODO: Add an option to configure this
+ private static final int MARQUEE_DELAY = 1200;
+ private static final int MARQUEE_RESTART_DELAY = 1200;
+ private static final int MARQUEE_RESOLUTION = 1000 / 30;
+ private static final int MARQUEE_PIXELS_PER_SECOND = 30;
+
+ private static final byte MARQUEE_STOPPED = 0x0;
+ private static final byte MARQUEE_STARTING = 0x1;
+ private static final byte MARQUEE_RUNNING = 0x2;
+
+ private static final int MESSAGE_START = 0x1;
+ private static final int MESSAGE_TICK = 0x2;
+ private static final int MESSAGE_RESTART = 0x3;
+
+ private final WeakReference<TextView> mView;
+
+ private byte mStatus = MARQUEE_STOPPED;
+ private float mScrollUnit;
+ private float mMaxScroll;
+ private int mRepeatLimit;
+
+ float mScroll;
+
+ Marquee(TextView v) {
+ final float density = v.getContext().getResources().getDisplayMetrics().density;
+ mScrollUnit = (MARQUEE_PIXELS_PER_SECOND * density) / (float) MARQUEE_RESOLUTION;
+ mView = new WeakReference<TextView>(v);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_START:
+ mStatus = MARQUEE_RUNNING;
+ tick();
+ break;
+ case MESSAGE_TICK:
+ tick();
+ break;
+ case MESSAGE_RESTART:
+ if (mStatus == MARQUEE_RUNNING) {
+ if (mRepeatLimit >= 0) {
+ mRepeatLimit--;
+ }
+ start(mRepeatLimit);
+ }
+ break;
+ }
+ }
+
+ void tick() {
+ if (mStatus != MARQUEE_RUNNING) {
+ return;
+ }
+
+ removeMessages(MESSAGE_TICK);
+
+ final TextView textView = mView.get();
+ if (textView != null && (textView.isFocused() || textView.isSelected())) {
+ mScroll += mScrollUnit;
+ if (mScroll > mMaxScroll) {
+ mScroll = mMaxScroll;
+ sendEmptyMessageDelayed(MESSAGE_RESTART, MARQUEE_RESTART_DELAY);
+ } else {
+ sendEmptyMessageDelayed(MESSAGE_TICK, MARQUEE_RESOLUTION);
+ }
+ textView.invalidate();
+ }
+ }
+
+ void stop() {
+ mStatus = MARQUEE_STOPPED;
+ removeMessages(MESSAGE_START);
+ removeMessages(MESSAGE_RESTART);
+ removeMessages(MESSAGE_TICK);
+ resetScroll();
+ }
+
+ private void resetScroll() {
+ mScroll = 0.0f;
+ final TextView textView = mView.get();
+ if (textView != null) textView.invalidate();
+ }
+
+ void start(int repeatLimit) {
+ if (repeatLimit == 0) {
+ stop();
+ return;
+ }
+ mRepeatLimit = repeatLimit;
+ final TextView textView = mView.get();
+ if (textView != null && textView.mLayout != null) {
+ mStatus = MARQUEE_STARTING;
+ mScroll = 0.0f;
+ mMaxScroll = textView.mLayout.getLineWidth(0) - (textView.getWidth() -
+ textView.getCompoundPaddingLeft() - textView.getCompoundPaddingRight());
+ textView.invalidate();
+ sendEmptyMessageDelayed(MESSAGE_START, MARQUEE_DELAY);
+ }
+ }
+
+ boolean isRunning() {
+ return mStatus == MARQUEE_RUNNING;
+ }
+
+ boolean isStopped() {
+ return mStatus == MARQUEE_STOPPED;
+ }
+ }
+
+ /**
+ * This method is called when the text is changed, in case any
+ * subclasses would like to know.
+ *
+ * @param text The text the TextView is displaying.
+ * @param start The offset of the start of the range of the text
+ * that was modified.
+ * @param before The offset of the former end of the range of the
+ * text that was modified. If text was simply inserted,
+ * this will be the same as <code>start</code>.
+ * If text was replaced with new text or deleted, the
+ * length of the old text was <code>before-start</code>.
+ * @param after The offset of the end of the range of the text
+ * that was modified. If text was simply deleted,
+ * this will be the same as <code>start</code>.
+ * If text was replaced with new text or inserted,
+ * the length of the new text is <code>after-start</code>.
+ */
+ protected void onTextChanged(CharSequence text,
+ int start, int before, int after) {
+ }
+
+ /**
+ * This method is called when the selection has changed, in case any
+ * subclasses would like to know.
+ *
+ * @param selStart The new selection start location.
+ * @param selEnd The new selection end location.
+ */
+ protected void onSelectionChanged(int selStart, int selEnd) {
+ }
+
+ /**
+ * Adds a TextWatcher to the list of those whose methods are called
+ * whenever this TextView's text changes.
+ * <p>
+ * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
+ * not called after {@link #setText} calls. Now, doing {@link #setText}
+ * if there are any text changed listeners forces the buffer type to
+ * Editable if it would not otherwise be and does call this method.
+ */
+ public void addTextChangedListener(TextWatcher watcher) {
+ if (mListeners == null) {
+ mListeners = new ArrayList<TextWatcher>();
+ }
+
+ mListeners.add(watcher);
+ }
+
+ /**
+ * Removes the specified TextWatcher from the list of those whose
+ * methods are called
+ * whenever this TextView's text changes.
+ */
+ public void removeTextChangedListener(TextWatcher watcher) {
+ if (mListeners != null) {
+ int i = mListeners.indexOf(watcher);
+
+ if (i >= 0) {
+ mListeners.remove(i);
+ }
+ }
+ }
+
+ private void sendBeforeTextChanged(CharSequence text, int start, int before,
+ int after) {
+ if (mListeners != null) {
+ final ArrayList<TextWatcher> list = mListeners;
+ final int count = list.size();
+ for (int i = 0; i < count; i++) {
+ list.get(i).beforeTextChanged(text, start, before, after);
+ }
+ }
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ void sendOnTextChanged(CharSequence text, int start, int before,
+ int after) {
+ if (mListeners != null) {
+ final ArrayList<TextWatcher> list = mListeners;
+ final int count = list.size();
+ for (int i = 0; i < count; i++) {
+ list.get(i).onTextChanged(text, start, before, after);
+ }
+ }
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ void sendAfterTextChanged(Editable text) {
+ if (mListeners != null) {
+ final ArrayList<TextWatcher> list = mListeners;
+ final int count = list.size();
+ for (int i = 0; i < count; i++) {
+ list.get(i).afterTextChanged(text);
+ }
+ }
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ void handleTextChanged(CharSequence buffer, int start,
+ int before, int after) {
+ final InputMethodState ims = mInputMethodState;
+ if (ims == null || ims.mBatchEditNesting == 0) {
+ updateAfterEdit();
+ }
+ if (ims != null) {
+ ims.mContentChanged = true;
+ if (ims.mChangedStart < 0) {
+ ims.mChangedStart = start;
+ ims.mChangedEnd = start+before;
+ } else {
+ if (ims.mChangedStart > start) ims.mChangedStart = start;
+ if (ims.mChangedEnd < (start+before)) ims.mChangedEnd = start+before;
+ }
+ ims.mChangedDelta += after-before;
+ }
+
+ sendOnTextChanged(buffer, start, before, after);
+ onTextChanged(buffer, start, before, after);
+ }
+
+ /**
+ * Not private so it can be called from an inner class without going
+ * through a thunk.
+ */
+ void spanChange(Spanned buf, Object what, int oldStart, int newStart,
+ int oldEnd, int newEnd) {
+ // XXX Make the start and end move together if this ends up
+ // spending too much time invalidating.
+
+ boolean selChanged = false;
+ int newSelStart=-1, newSelEnd=-1;
+
+ final InputMethodState ims = mInputMethodState;
+
+ if (what == Selection.SELECTION_END) {
+ mHighlightPathBogus = true;
+ selChanged = true;
+ newSelEnd = newStart;
+
+ if (!isFocused()) {
+ mSelectionMoved = true;
+ }
+
+ if (oldStart >= 0 || newStart >= 0) {
+ invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
+ registerForPreDraw();
+
+ if (isFocused()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ makeBlink();
+ }
+ }
+ }
+
+ if (what == Selection.SELECTION_START) {
+ mHighlightPathBogus = true;
+ selChanged = true;
+ newSelStart = newStart;
+
+ if (!isFocused()) {
+ mSelectionMoved = true;
+ }
+
+ if (oldStart >= 0 || newStart >= 0) {
+ int end = Selection.getSelectionEnd(buf);
+ invalidateCursor(end, oldStart, newStart);
+ }
+ }
+
+ if (selChanged) {
+ if ((buf.getSpanFlags(what)&Spanned.SPAN_INTERMEDIATE) == 0) {
+ if (newSelStart < 0) {
+ newSelStart = Selection.getSelectionStart(buf);
+ }
+ if (newSelEnd < 0) {
+ newSelEnd = Selection.getSelectionEnd(buf);
+ }
+ onSelectionChanged(newSelStart, newSelEnd);
+ }
+ }
+
+ if (what instanceof UpdateAppearance ||
+ what instanceof ParagraphStyle) {
+ if (ims == null || ims.mBatchEditNesting == 0) {
+ invalidate();
+ mHighlightPathBogus = true;
+ checkForResize();
+ } else {
+ ims.mContentChanged = true;
+ }
+ }
+
+ if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
+ mHighlightPathBogus = true;
+
+ if (Selection.getSelectionStart(buf) >= 0) {
+ if (ims == null || ims.mBatchEditNesting == 0) {
+ invalidateCursor();
+ } else {
+ ims.mCursorChanged = true;
+ }
+ }
+ }
+
+ if (what instanceof ParcelableSpan) {
+ // If this is a span that can be sent to a remote process,
+ // the current extract editor would be interested in it.
+ if (ims != null && ims.mExtracting != null) {
+ if (ims.mBatchEditNesting != 0) {
+ if (oldStart >= 0) {
+ if (ims.mChangedStart > oldStart) {
+ ims.mChangedStart = oldStart;
+ }
+ if (ims.mChangedStart > oldEnd) {
+ ims.mChangedStart = oldEnd;
+ }
+ }
+ if (newStart >= 0) {
+ if (ims.mChangedStart > newStart) {
+ ims.mChangedStart = newStart;
+ }
+ if (ims.mChangedStart > newEnd) {
+ ims.mChangedStart = newEnd;
+ }
+ }
+ } else {
+ if (DEBUG_EXTRACT) Log.v(TAG, "Span change outside of batch: "
+ + oldStart + "-" + oldEnd + ","
+ + newStart + "-" + newEnd + what);
+ ims.mContentChanged = true;
+ }
+ }
+ }
+ }
+
+ private class ChangeWatcher
+ implements TextWatcher, SpanWatcher {
+ public void beforeTextChanged(CharSequence buffer, int start,
+ int before, int after) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "beforeTextChanged start=" + start
+ + " before=" + before + " after=" + after + ": " + buffer);
+ TextView.this.sendBeforeTextChanged(buffer, start, before, after);
+ }
+
+ public void onTextChanged(CharSequence buffer, int start,
+ int before, int after) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "onTextChanged start=" + start
+ + " before=" + before + " after=" + after + ": " + buffer);
+ TextView.this.handleTextChanged(buffer, start, before, after);
+ }
+
+ public void afterTextChanged(Editable buffer) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "afterTextChanged: " + buffer);
+ TextView.this.sendAfterTextChanged(buffer);
+
+ if (MetaKeyKeyListener.getMetaState(buffer,
+ MetaKeyKeyListener.META_SELECTING) != 0) {
+ MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
+ }
+ }
+
+ public void onSpanChanged(Spannable buf,
+ Object what, int s, int e, int st, int en) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "onSpanChanged s=" + s + " e=" + e
+ + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
+ TextView.this.spanChange(buf, what, s, st, e, en);
+ }
+
+ public void onSpanAdded(Spannable buf, Object what, int s, int e) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "onSpanAdded s=" + s + " e=" + e
+ + " what=" + what + ": " + buf);
+ TextView.this.spanChange(buf, what, -1, s, -1, e);
+ }
+
+ public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
+ if (DEBUG_EXTRACT) Log.v(TAG, "onSpanRemoved s=" + s + " e=" + e
+ + " what=" + what + ": " + buf);
+ TextView.this.spanChange(buf, what, s, -1, e, -1);
+ }
+ }
+
+ private void makeBlink() {
+ if (!mCursorVisible) {
+ if (mBlink != null) {
+ mBlink.removeCallbacks(mBlink);
+ }
+
+ return;
+ }
+
+ if (mBlink == null)
+ mBlink = new Blink(this);
+
+ mBlink.removeCallbacks(mBlink);
+ mBlink.postAtTime(mBlink, mShowCursor + BLINK);
+ }
+
+ @Override
+ public void onStartTemporaryDetach() {
+ mTemporaryDetach = true;
+ }
+
+ @Override
+ public void onFinishTemporaryDetach() {
+ mTemporaryDetach = false;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ if (mTemporaryDetach) {
+ // If we are temporarily in the detach state, then do nothing.
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ return;
+ }
+
+ mShowCursor = SystemClock.uptimeMillis();
+
+ ensureEndedBatchEdit();
+
+ if (focused) {
+ int selStart = getSelectionStart();
+ int selEnd = getSelectionEnd();
+
+ if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
+ boolean selMoved = mSelectionMoved;
+
+ if (mMovement != null) {
+ mMovement.onTakeFocus(this, (Spannable) mText, direction);
+ }
+
+ if (mSelectAllOnFocus) {
+ Selection.setSelection((Spannable) mText, 0, mText.length());
+ }
+
+ if (selMoved && selStart >= 0 && selEnd >= 0) {
+ /*
+ * Someone intentionally set the selection, so let them
+ * do whatever it is that they wanted to do instead of
+ * the default on-focus behavior. We reset the selection
+ * here instead of just skipping the onTakeFocus() call
+ * because some movement methods do something other than
+ * just setting the selection in theirs and we still
+ * need to go through that path.
+ */
+
+ Selection.setSelection((Spannable) mText, selStart, selEnd);
+ }
+ }
+
+ mFrozenWithFocus = false;
+ mSelectionMoved = false;
+
+ if (mText instanceof Spannable) {
+ Spannable sp = (Spannable) mText;
+ MetaKeyKeyListener.resetMetaState(sp);
+ }
+
+ makeBlink();
+
+ if (mError != null) {
+ showError();
+ }
+ } else {
+ if (mError != null) {
+ hideError();
+ }
+ // Don't leave us in the middle of a batch edit.
+ onEndBatchEdit();
+ }
+
+ startStopMarquee(focused);
+
+ if (mTransformation != null) {
+ mTransformation.onFocusChanged(this, mText, focused, direction,
+ previouslyFocusedRect);
+ }
+
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ super.onWindowFocusChanged(hasWindowFocus);
+
+ if (hasWindowFocus) {
+ if (mBlink != null) {
+ mBlink.uncancel();
+
+ if (isFocused()) {
+ mShowCursor = SystemClock.uptimeMillis();
+ makeBlink();
+ }
+ }
+ } else {
+ if (mBlink != null) {
+ mBlink.cancel();
+ }
+ // Don't leave us in the middle of a batch edit.
+ onEndBatchEdit();
+ if (mInputContentType != null) {
+ mInputContentType.enterDown = false;
+ }
+ }
+
+ startStopMarquee(hasWindowFocus);
+ }
+
+ /**
+ * Use {@link BaseInputConnection#removeComposingSpans
+ * BaseInputConnection.removeComposingSpans()} to remove any IME composing
+ * state from this text view.
+ */
+ public void clearComposingText() {
+ if (mText instanceof Spannable) {
+ BaseInputConnection.removeComposingSpans((Spannable)mText);
+ }
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ boolean wasSelected = isSelected();
+
+ super.setSelected(selected);
+
+ if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (selected) {
+ startMarquee();
+ } else {
+ stopMarquee();
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final boolean superResult = super.onTouchEvent(event);
+
+ final int action = event.getAction();
+
+ /*
+ * Don't handle the release after a long press, because it will
+ * move the selection away from whatever the menu action was
+ * trying to affect.
+ */
+ if (mEatTouchRelease && action == MotionEvent.ACTION_UP) {
+ mEatTouchRelease = false;
+ return superResult;
+ }
+
+ if (mMovement != null && mText instanceof Spannable && mLayout != null) {
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mScrolled = false;
+ }
+
+ boolean moved = mMovement.onTouchEvent(this, (Spannable) mText, event);
+
+ if (mText instanceof Editable && onCheckIsTextEditor()) {
+ if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) {
+ InputMethodManager imm = (InputMethodManager)
+ getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(this, 0);
+ }
+ }
+
+ if (moved) {
+ return true;
+ }
+ }
+
+ return superResult;
+ }
+
+ @Override
+ public void cancelLongPress() {
+ super.cancelLongPress();
+ mScrolled = true;
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent event) {
+ if (mMovement != null && mText instanceof Spannable &&
+ mLayout != null) {
+ if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
+ return true;
+ }
+ }
+
+ return super.onTrackballEvent(event);
+ }
+
+ public void setScroller(Scroller s) {
+ mScroller = s;
+ }
+
+ private static class Blink extends Handler implements Runnable {
+ private WeakReference<TextView> mView;
+ private boolean mCancelled;
+
+ public Blink(TextView v) {
+ mView = new WeakReference<TextView>(v);
+ }
+
+ public void run() {
+ if (mCancelled) {
+ return;
+ }
+
+ removeCallbacks(Blink.this);
+
+ TextView tv = mView.get();
+
+ if (tv != null && tv.isFocused()) {
+ int st = Selection.getSelectionStart(tv.mText);
+ int en = Selection.getSelectionEnd(tv.mText);
+
+ if (st == en && st >= 0 && en >= 0) {
+ if (tv.mLayout != null) {
+ tv.invalidateCursorPath();
+ }
+
+ postAtTime(this, SystemClock.uptimeMillis() + BLINK);
+ }
+ }
+ }
+
+ void cancel() {
+ if (!mCancelled) {
+ removeCallbacks(Blink.this);
+ mCancelled = true;
+ }
+ }
+
+ void uncancel() {
+ mCancelled = false;
+ }
+ }
+
+ @Override
+ protected float getLeftFadingEdgeStrength() {
+ if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (mMarquee != null && !mMarquee.isStopped()) {
+ final Marquee marquee = mMarquee;
+ return marquee.mScroll / getHorizontalFadingEdgeLength();
+ } else if (getLineCount() == 1) {
+ switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ return 0.0f;
+ case Gravity.RIGHT:
+ return (mLayout.getLineRight(0) - (mRight - mLeft) -
+ getCompoundPaddingLeft() - getCompoundPaddingRight() -
+ mLayout.getLineLeft(0)) / getHorizontalFadingEdgeLength();
+ case Gravity.CENTER_HORIZONTAL:
+ return 0.0f;
+ }
+ }
+ }
+ return super.getLeftFadingEdgeStrength();
+ }
+
+ @Override
+ protected float getRightFadingEdgeStrength() {
+ if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+ if (mMarquee != null && !mMarquee.isStopped()) {
+ final Marquee marquee = mMarquee;
+ return (marquee.mMaxScroll - marquee.mScroll) / getHorizontalFadingEdgeLength();
+ } else if (getLineCount() == 1) {
+ switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.LEFT:
+ return (mLayout.getLineRight(0) - mScrollX - (mRight - mLeft) -
+ getCompoundPaddingLeft() - getCompoundPaddingRight()) /
+ getHorizontalFadingEdgeLength();
+ case Gravity.RIGHT:
+ return 0.0f;
+ case Gravity.CENTER_HORIZONTAL:
+ return (mLayout.getLineWidth(0) - ((mRight - mLeft) -
+ getCompoundPaddingLeft() - getCompoundPaddingRight())) /
+ getHorizontalFadingEdgeLength();
+ }
+ }
+ }
+ return super.getRightFadingEdgeStrength();
+ }
+
+ @Override
+ protected int computeHorizontalScrollRange() {
+ if (mLayout != null)
+ return mLayout.getWidth();
+
+ return super.computeHorizontalScrollRange();
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ if (mLayout != null)
+ return mLayout.getHeight();
+
+ return super.computeVerticalScrollRange();
+ }
+
+ public enum BufferType {
+ NORMAL, SPANNABLE, EDITABLE,
+ }
+
+ /**
+ * Returns the TextView_textColor attribute from the
+ * Resources.StyledAttributes, if set, or the TextAppearance_textColor
+ * from the TextView_textAppearance attribute, if TextView_textColor
+ * was not set directly.
+ */
+ public static ColorStateList getTextColors(Context context, TypedArray attrs) {
+ ColorStateList colors;
+ colors = attrs.getColorStateList(com.android.internal.R.styleable.
+ TextView_textColor);
+
+ if (colors == null) {
+ int ap = attrs.getResourceId(com.android.internal.R.styleable.
+ TextView_textAppearance, -1);
+ if (ap != -1) {
+ TypedArray appearance;
+ appearance = context.obtainStyledAttributes(ap,
+ com.android.internal.R.styleable.TextAppearance);
+ colors = appearance.getColorStateList(com.android.internal.R.styleable.
+ TextAppearance_textColor);
+ appearance.recycle();
+ }
+ }
+
+ return colors;
+ }
+
+ /**
+ * Returns the default color from the TextView_textColor attribute
+ * from the AttributeSet, if set, or the default color from the
+ * TextAppearance_textColor from the TextView_textAppearance attribute,
+ * if TextView_textColor was not set directly.
+ */
+ public static int getTextColor(Context context,
+ TypedArray attrs,
+ int def) {
+ ColorStateList colors = getTextColors(context, attrs);
+
+ if (colors == null) {
+ return def;
+ } else {
+ return colors.getDefaultColor();
+ }
+ }
+
+ @Override
+ public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_A:
+ if (canSelectAll()) {
+ return onTextContextMenuItem(ID_SELECT_ALL);
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_X:
+ if (canCut()) {
+ return onTextContextMenuItem(ID_CUT);
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_C:
+ if (canCopy()) {
+ return onTextContextMenuItem(ID_COPY);
+ }
+
+ break;
+
+ case KeyEvent.KEYCODE_V:
+ if (canPaste()) {
+ return onTextContextMenuItem(ID_PASTE);
+ }
+
+ break;
+ }
+
+ return super.onKeyShortcut(keyCode, event);
+ }
+
+ private boolean canSelectAll() {
+ if (mText instanceof Spannable && mText.length() != 0 &&
+ mMovement != null && mMovement.canSelectArbitrarily()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean canSelectText() {
+ if (mText instanceof Spannable && mText.length() != 0 &&
+ mMovement != null && mMovement.canSelectArbitrarily()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean canCut() {
+ if (mTransformation instanceof PasswordTransformationMethod) {
+ return false;
+ }
+
+ if (mText.length() > 0 && getSelectionStart() >= 0) {
+ if (mText instanceof Editable && mInput != null) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean canCopy() {
+ if (mTransformation instanceof PasswordTransformationMethod) {
+ return false;
+ }
+
+ if (mText.length() > 0 && getSelectionStart() >= 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean canPaste() {
+ if (mText instanceof Editable && mInput != null &&
+ getSelectionStart() >= 0 && getSelectionEnd() >= 0) {
+ ClipboardManager clip = (ClipboardManager)getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ if (clip.hasText()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a word to add to the dictionary from the context menu,
+ * or null if there is no cursor or no word at the cursor.
+ */
+ private String getWordForDictionary() {
+ int end = getSelectionEnd();
+
+ if (end < 0) {
+ return null;
+ }
+
+ int start = end;
+ int len = mText.length();
+
+ for (; start > 0; start--) {
+ char c = mTransformed.charAt(start - 1);
+ int type = Character.getType(c);
+
+ if (c != '\'' &&
+ type != Character.UPPERCASE_LETTER &&
+ type != Character.LOWERCASE_LETTER &&
+ type != Character.TITLECASE_LETTER &&
+ type != Character.MODIFIER_LETTER &&
+ type != Character.DECIMAL_DIGIT_NUMBER) {
+ break;
+ }
+ }
+
+ for (; end < len; end++) {
+ char c = mTransformed.charAt(end);
+ int type = Character.getType(c);
+
+ if (c != '\'' &&
+ type != Character.UPPERCASE_LETTER &&
+ type != Character.LOWERCASE_LETTER &&
+ type != Character.TITLECASE_LETTER &&
+ type != Character.MODIFIER_LETTER &&
+ type != Character.DECIMAL_DIGIT_NUMBER) {
+ break;
+ }
+ }
+
+ if (start == end) {
+ return null;
+ }
+
+ if (end - start > 48) {
+ return null;
+ }
+
+ return TextUtils.substring(mTransformed, start, end);
+ }
+
+ @Override
+ protected void onCreateContextMenu(ContextMenu menu) {
+ super.onCreateContextMenu(menu);
+ boolean added = false;
+
+ if (!isFocused()) {
+ if (isFocusable() && mInput != null) {
+ if (canCopy()) {
+ MenuHandler handler = new MenuHandler();
+ int name = com.android.internal.R.string.copyAll;
+
+ menu.add(0, ID_COPY, 0, name).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('c');
+ menu.setHeaderTitle(com.android.internal.R.string.
+ editTextMenuTitle);
+ }
+ }
+
+ return;
+ }
+
+ MenuHandler handler = new MenuHandler();
+
+ if (canSelectAll()) {
+ menu.add(0, ID_SELECT_ALL, 0,
+ com.android.internal.R.string.selectAll).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('a');
+ added = true;
+ }
+
+ boolean selection = getSelectionStart() != getSelectionEnd();
+
+ if (canSelectText()) {
+ if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
+ menu.add(0, ID_STOP_SELECTING_TEXT, 0,
+ com.android.internal.R.string.stopSelectingText).
+ setOnMenuItemClickListener(handler);
+ added = true;
+ } else {
+ menu.add(0, ID_START_SELECTING_TEXT, 0,
+ com.android.internal.R.string.selectText).
+ setOnMenuItemClickListener(handler);
+ added = true;
+ }
+ }
+
+ if (canCut()) {
+ int name;
+ if (selection) {
+ name = com.android.internal.R.string.cut;
+ } else {
+ name = com.android.internal.R.string.cutAll;
+ }
+
+ menu.add(0, ID_CUT, 0, name).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('x');
+ added = true;
+ }
+
+ if (canCopy()) {
+ int name;
+ if (selection) {
+ name = com.android.internal.R.string.copy;
+ } else {
+ name = com.android.internal.R.string.copyAll;
+ }
+
+ menu.add(0, ID_COPY, 0, name).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('c');
+ added = true;
+ }
+
+ if (canPaste()) {
+ menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
+ setOnMenuItemClickListener(handler).
+ setAlphabeticShortcut('v');
+ added = true;
+ }
+
+ if (mText instanceof Spanned) {
+ int selStart = getSelectionStart();
+ int selEnd = getSelectionEnd();
+
+ int min = Math.min(selStart, selEnd);
+ int max = Math.max(selStart, selEnd);
+
+ URLSpan[] urls = ((Spanned) mText).getSpans(min, max,
+ URLSpan.class);
+ if (urls.length == 1) {
+ menu.add(0, ID_COPY_URL, 0,
+ com.android.internal.R.string.copyUrl).
+ setOnMenuItemClickListener(handler);
+ added = true;
+ }
+ }
+
+ if (isInputMethodTarget()) {
+ menu.add(1, ID_SWITCH_INPUT_METHOD, 0, com.android.internal.R.string.inputMethod).
+ setOnMenuItemClickListener(handler);
+ added = true;
+ }
+
+ String word = getWordForDictionary();
+ if (word != null) {
+ menu.add(1, ID_ADD_TO_DICTIONARY, 0,
+ getContext().getString(com.android.internal.R.string.addToDictionary, word)).
+ setOnMenuItemClickListener(handler);
+ added = true;
+
+ }
+
+ if (added) {
+ menu.setHeaderTitle(com.android.internal.R.string.editTextMenuTitle);
+ }
+ }
+
+ /**
+ * Returns whether this text view is a current input method target. The
+ * default implementation just checks with {@link InputMethodManager}.
+ */
+ public boolean isInputMethodTarget() {
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ return imm != null && imm.isActive(this);
+ }
+
+ private static final int ID_SELECT_ALL = android.R.id.selectAll;
+ private static final int ID_START_SELECTING_TEXT = android.R.id.startSelectingText;
+ private static final int ID_STOP_SELECTING_TEXT = android.R.id.stopSelectingText;
+ private static final int ID_CUT = android.R.id.cut;
+ private static final int ID_COPY = android.R.id.copy;
+ private static final int ID_PASTE = android.R.id.paste;
+ private static final int ID_COPY_URL = android.R.id.copyUrl;
+ private static final int ID_SWITCH_INPUT_METHOD = android.R.id.switchInputMethod;
+ private static final int ID_ADD_TO_DICTIONARY = android.R.id.addToDictionary;
+
+ private class MenuHandler implements MenuItem.OnMenuItemClickListener {
+ public boolean onMenuItemClick(MenuItem item) {
+ return onTextContextMenuItem(item.getItemId());
+ }
+ }
+
+ /**
+ * Called when a context menu option for the text view is selected. Currently
+ * this will be one of: {@link android.R.id#selectAll},
+ * {@link android.R.id#startSelectingText}, {@link android.R.id#stopSelectingText},
+ * {@link android.R.id#cut}, {@link android.R.id#copy},
+ * {@link android.R.id#paste}, {@link android.R.id#copyUrl},
+ * or {@link android.R.id#switchInputMethod}.
+ */
+ public boolean onTextContextMenuItem(int id) {
+ int selStart = getSelectionStart();
+ int selEnd = getSelectionEnd();
+
+ if (!isFocused()) {
+ selStart = 0;
+ selEnd = mText.length();
+ }
+
+ int min = Math.min(selStart, selEnd);
+ int max = Math.max(selStart, selEnd);
+
+ if (min < 0) {
+ min = 0;
+ }
+ if (max < 0) {
+ max = 0;
+ }
+
+ ClipboardManager clip = (ClipboardManager)getContext()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+
+ switch (id) {
+ case ID_SELECT_ALL:
+ Selection.setSelection((Spannable) mText, 0,
+ mText.length());
+ return true;
+
+ case ID_START_SELECTING_TEXT:
+ MetaKeyKeyListener.startSelecting(this, (Spannable) mText);
+ return true;
+
+ case ID_STOP_SELECTING_TEXT:
+ MetaKeyKeyListener.stopSelecting(this, (Spannable) mText);
+ Selection.setSelection((Spannable) mText, getSelectionEnd());
+ return true;
+
+ case ID_CUT:
+ MetaKeyKeyListener.stopSelecting(this, (Spannable) mText);
+
+ if (min == max) {
+ min = 0;
+ max = mText.length();
+ }
+
+ clip.setText(mTransformed.subSequence(min, max));
+ ((Editable) mText).delete(min, max);
+ return true;
+
+ case ID_COPY:
+ MetaKeyKeyListener.stopSelecting(this, (Spannable) mText);
+
+ if (min == max) {
+ min = 0;
+ max = mText.length();
+ }
+
+ clip.setText(mTransformed.subSequence(min, max));
+ return true;
+
+ case ID_PASTE:
+ MetaKeyKeyListener.stopSelecting(this, (Spannable) mText);
+
+ CharSequence paste = clip.getText();
+
+ if (paste != null) {
+ Selection.setSelection((Spannable) mText, max);
+ ((Editable) mText).replace(min, max, paste);
+ }
+
+ return true;
+
+ case ID_COPY_URL:
+ MetaKeyKeyListener.stopSelecting(this, (Spannable) mText);
+
+ URLSpan[] urls = ((Spanned) mText).getSpans(min, max,
+ URLSpan.class);
+ if (urls.length == 1) {
+ clip.setText(urls[0].getURL());
+ }
+
+ return true;
+
+ case ID_SWITCH_INPUT_METHOD:
+ InputMethodManager imm = InputMethodManager.peekInstance();
+ if (imm != null) {
+ imm.showInputMethodPicker();
+ }
+ return true;
+
+ case ID_ADD_TO_DICTIONARY:
+ String word = getWordForDictionary();
+
+ if (word != null) {
+ Intent i = new Intent("com.android.settings.USER_DICTIONARY_INSERT");
+ i.putExtra("word", word);
+ getContext().startActivity(i);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean performLongClick() {
+ if (super.performLongClick()) {
+ mEatTouchRelease = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ @ViewDebug.ExportedProperty
+ private CharSequence mText;
+ private CharSequence mTransformed;
+ private BufferType mBufferType = BufferType.NORMAL;
+
+ private int mInputType = EditorInfo.TYPE_NULL;
+ private CharSequence mHint;
+ private Layout mHintLayout;
+
+ private KeyListener mInput;
+
+ private MovementMethod mMovement;
+ private TransformationMethod mTransformation;
+ private ChangeWatcher mChangeWatcher;
+
+ private ArrayList<TextWatcher> mListeners = null;
+
+ // display attributes
+ private TextPaint mTextPaint;
+ private Paint mHighlightPaint;
+ private int mHighlightColor = 0xFFBBDDFF;
+ private Layout mLayout;
+
+ private long mShowCursor;
+ private Blink mBlink;
+ private boolean mCursorVisible = true;
+
+ private boolean mSelectAllOnFocus = false;
+
+ private int mGravity = Gravity.TOP | Gravity.LEFT;
+ private boolean mHorizontallyScrolling;
+
+ private int mAutoLinkMask;
+ private boolean mLinksClickable = true;
+
+ private float mSpacingMult = 1;
+ private float mSpacingAdd = 0;
+
+ private static final int LINES = 1;
+ private static final int EMS = LINES;
+ private static final int PIXELS = 2;
+
+ private int mMaximum = Integer.MAX_VALUE;
+ private int mMaxMode = LINES;
+ private int mMinimum = 0;
+ private int mMinMode = LINES;
+
+ private int mMaxWidth = Integer.MAX_VALUE;
+ private int mMaxWidthMode = PIXELS;
+ private int mMinWidth = 0;
+ private int mMinWidthMode = PIXELS;
+
+ private boolean mSingleLine;
+ private int mDesiredHeightAtMeasure = -1;
+ private boolean mIncludePad = true;
+
+ // tmp primitives, so we don't alloc them on each draw
+ private Path mHighlightPath;
+ private boolean mHighlightPathBogus = true;
+ private static final RectF sTempRect = new RectF();
+
+ // XXX should be much larger
+ private static final int VERY_WIDE = 16384;
+
+ private static final int BLINK = 500;
+
+ private static final int ANIMATED_SCROLL_GAP = 250;
+ private long mLastScroll;
+ private Scroller mScroller = null;
+
+ private BoringLayout.Metrics mBoring;
+ private BoringLayout.Metrics mHintBoring;
+
+ private BoringLayout mSavedLayout, mSavedHintLayout;
+
+ private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+ private InputFilter[] mFilters = NO_FILTERS;
+ private static final Spanned EMPTY_SPANNED = new SpannedString("");
+}