/* * 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 com.android.internal.util.FastMath; import com.android.internal.widget.EditableInputConnection; import org.xmlpull.v1.XmlPullParserException; import android.R; import android.content.ClipData; import android.content.ClipData.Item; import android.content.ClipboardManager; import android.content.Context; import android.content.pm.PackageManager; 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.Color; 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.inputmethodservice.ExtractEditText; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.text.BoringLayout; import android.text.DynamicLayout; import android.text.Editable; import android.text.GetChars; import android.text.GraphicsOperations; import android.text.InputFilter; import android.text.InputType; import android.text.Layout; import android.text.ParcelableSpan; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; import android.text.TextDirectionHeuristic; import android.text.TextDirectionHeuristics; import android.text.TextDirectionHeuristics.AnyStrong; import android.text.TextDirectionHeuristics.CharCount; import android.text.TextDirectionHeuristics.FirstStrong; import android.text.TextDirectionHeuristics.TextDirectionAlgorithm; import android.text.TextDirectionHeuristics.TextDirectionHeuristicImpl; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.AllCapsTransformationMethod; import android.text.method.ArrowKeyMovementMethod; 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.PasswordTransformationMethod; import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; import android.text.method.TransformationMethod2; import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; import android.text.style.SuggestionSpan; import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.FloatMath; import android.util.Log; import android.util.TypedValue; import android.view.ActionMode; import android.view.ActionMode.Callback; import android.view.ContextMenu; import android.view.DragEvent; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewRootImpl; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.CorrectionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.RemoteViews.RemoteView; import java.io.IOException; import java.lang.ref.WeakReference; import java.text.BreakIterator; import java.util.ArrayList; /** * 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. * *
* XML attributes *
* 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_textAppearance * @attr ref android.R.styleable#TextView_textColorLink * @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_freezesText * @attr ref android.R.styleable#TextView_ellipsize * @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_drawablePadding * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier * @attr ref android.R.styleable#TextView_marqueeRepeatLimit * @attr ref android.R.styleable#TextView_inputType * @attr ref android.R.styleable#TextView_imeOptions * @attr ref android.R.styleable#TextView_privateImeOptions * @attr ref android.R.styleable#TextView_imeActionLabel * @attr ref android.R.styleable#TextView_imeActionId * @attr ref android.R.styleable#TextView_editorExtras */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { static final String LOG_TAG = "TextView"; static final boolean DEBUG_EXTRACT = false; private static final int PRIORITY = 100; private int mCurrentAlpha = 255; final int[] mTempCoords = new int[2]; Rect mTempRect; 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 mDispatchTemporaryDetach; private boolean mDiscardNextActionUp = false; private boolean mIgnoreActionUpEvent = 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, mDrawableStart, mDrawableEnd; int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight, mDrawableSizeStart, mDrawableSizeEnd; int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight, mDrawableHeightStart, mDrawableHeightEnd; int mDrawablePadding; } private Drawables mDrawables; private CharSequence mError; private boolean mErrorWasChanged; private ErrorPopup 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 boolean mTouchFocusSelected = false; private Marquee mMarquee; private boolean mRestartMarquee; private int mMarqueeRepeatLimit = 3; class InputContentType { int imeOptions = EditorInfo.IME_NULL; 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 mSelectionModeChanged; boolean mContentChanged; int mChangedStart, mChangedEnd, mChangedDelta; } InputMethodState mInputMethodState; private int mTextSelectHandleLeftRes; private int mTextSelectHandleRightRes; private int mTextSelectHandleRes; private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout; private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout; private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; private int mTextEditSuggestionItemLayout; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionRangeSpan mSuggestionRangeSpan; private boolean mSuggestionsEnabled = true; private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2 private Drawable mSelectHandleLeft; private Drawable mSelectHandleRight; private Drawable mSelectHandleCenter; private float mLastDownPositionX, mLastDownPositionY; private Callback mCustomSelectionActionModeCallback; private final int mSquaredTouchSlopDistance; // Set when this TextView gained focus with some text selected. Will start selection mode. private boolean mCreatedWithASelection = false; private WordIterator mWordIterator; // The alignment to pass to Layout, or null if not resolved. private Layout.Alignment mLayoutAlignment; // The default value for mTextAlign. private TextAlign mTextAlign = TextAlign.INHERIT; private static enum TextAlign { INHERIT, GRAVITY, TEXT_START, TEXT_END, CENTER, VIEW_START, VIEW_END; } private boolean bResolvedDrawables = false; /* * 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_NULL * EditorInfo.IME_NULL} 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); } @SuppressWarnings("deprecation") public TextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mText = ""; mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mTextPaint.density = getResources().getDisplayMetrics().density; mTextPaint.setCompatibilityScaling( getResources().getCompatibilityInfo().applicationScale); // 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); mHighlightPaint.setCompatibilityScaling( getResources().getCompatibilityInfo().applicationScale); 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; boolean allCaps = false; /* * 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; case com.android.internal.R.styleable.TextAppearance_textAllCaps: allCaps = appearance.getBoolean(attr, false); 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, drawableStart = null, drawableEnd = null; int drawablePadding = 0; int ellipsize = -1; boolean singleLine = false; int maxlength = -1; CharSequence text = ""; CharSequence hint = null; 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_drawableStart: drawableStart = a.getDrawable(attr); break; case com.android.internal.R.styleable.TextView_drawableEnd: drawableEnd = 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: hint = 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(LOG_TAG, "Failure reading input extras", e); } catch (IOException e) { Log.w(LOG_TAG, "Failure reading input extras", e); } break; case com.android.internal.R.styleable.TextView_textCursorDrawable: mCursorDrawableRes = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textSelectHandleLeft: mTextSelectHandleLeftRes = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textSelectHandleRight: mTextSelectHandleRightRes = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textSelectHandle: mTextSelectHandleRes = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textEditPasteWindowLayout: mTextEditPasteWindowLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textEditNoPasteWindowLayout: mTextEditNoPasteWindowLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textEditSidePasteWindowLayout: mTextEditSidePasteWindowLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textEditSideNoPasteWindowLayout: mTextEditSideNoPasteWindowLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textEditSuggestionsBottomWindowLayout: mTextEditSuggestionsBottomWindowLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textEditSuggestionsTopWindowLayout: mTextEditSuggestionsTopWindowLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout: mTextEditSuggestionItemLayout = a.getResourceId(attr, 0); break; case com.android.internal.R.styleable.TextView_textIsSelectable: mTextIsSelectable = a.getBoolean(attr, false); break; case com.android.internal.R.styleable.TextView_suggestionsEnabled: mSuggestionsEnabled = a.getBoolean(attr, true); break; case com.android.internal.R.styleable.TextView_textAllCaps: allCaps = a.getBoolean(attr, false); break; } } a.recycle(); BufferType bufferType = BufferType.EDITABLE; final int variation = inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION); final boolean passwordInputType = variation == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD); final boolean webPasswordInputType = variation == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD); final boolean numberPasswordInputType = variation == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD); 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()); // If no input type was specified, we will default to generic // text, since we can't tell the IME about the set of digits // that was selected. mInputType = inputType != EditorInfo.TYPE_NULL ? inputType : EditorInfo.TYPE_CLASS_TEXT; } else if (inputType != EditorInfo.TYPE_NULL) { setInputType(inputType, true); // If set, the input type overrides what was set using the deprecated singleLine flag. singleLine = !isMultilineInputType(inputType); } else if (phone) { mInput = DialerKeyListener.getInstance(); mInputType = 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; 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 (mTextIsSelectable) { // Prevent text changes from keyboard. mInputType = EditorInfo.TYPE_NULL; mInput = null; bufferType = BufferType.SPANNABLE; // Required to request focus while in touch mode. setFocusableInTouchMode(true); // So that selection can be changed using arrow keys and touch is handled. setMovementMethod(ArrowKeyMovementMethod.getInstance()); } 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; } } // mInputType has been set from inputType, possibly modified by mInputMethod. // Specialize mInputType to [web]password if we have a text class and the original input // type was a password. if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { if (password || passwordInputType) { mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; } if (webPasswordInputType) { mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; } } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) { if (numberPasswordInputType) { mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD; } } if (selectallonfocus) { mSelectAllOnFocus = true; if (bufferType == BufferType.NORMAL) bufferType = BufferType.SPANNABLE; } setCompoundDrawablesWithIntrinsicBounds( drawableLeft, drawableTop, drawableRight, drawableBottom); setRelativeDrawablesIfNeeded(drawableStart, drawableEnd); setCompoundDrawablePadding(drawablePadding); // Same as setSingleLine(), but make sure the transformation method and the maximum number // of lines of height are unchanged for multi-line TextViews. setInputTypeSingleLine(singleLine); applySingleLine(singleLine, singleLine, singleLine); if (singleLine && 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 (allCaps) { setTransformationMethod(new AllCapsTransformationMethod(getContext())); } if (password || passwordInputType || webPasswordInputType || numberPasswordInputType) { setTransformationMethod(PasswordTransformationMethod.getInstance()); typefaceIndex = MONOSPACE; } else if ((mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION)) == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)) { 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); if (hint != null) setHint(hint); /* * 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); prepareCursorControllers(); final ViewConfiguration viewConfiguration = ViewConfiguration.get(context); final int touchSlop = viewConfiguration.getScaledTouchSlop(); mSquaredTouchSlopDistance = touchSlop * touchSlop; } 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); } private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) { boolean hasRelativeDrawables = (start != null) || (end != null); if (hasRelativeDrawables) { Drawables dr = mDrawables; if (dr == null) { mDrawables = dr = new Drawables(); } final Rect compoundRect = dr.mCompoundRect; int[] state = getDrawableState(); if (start != null) { start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight()); start.setState(state); start.copyBounds(compoundRect); start.setCallback(this); dr.mDrawableStart = start; dr.mDrawableSizeStart = compoundRect.width(); dr.mDrawableHeightStart = compoundRect.height(); } else { dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; } if (end != null) { end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight()); end.setState(state); end.copyBounds(compoundRect); end.setCallback(this); dr.mDrawableEnd = end; dr.mDrawableSizeEnd = compoundRect.width(); dr.mDrawableHeightEnd = compoundRect.height(); } else { dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; } } } @Override public void setEnabled(boolean enabled) { if (enabled == isEnabled()) { return; } if (!enabled) { // Hide the soft input if the currently active TextView is disabled InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && imm.isActive(this)) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } } super.setEnabled(enabled); prepareCursorControllers(); } /** * 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. *
* 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; } // Change inputType, without affecting transformation. // No need to applySingleLine since mSingleLine is unchanged. setInputTypeSingleLine(mSingleLine); } 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. *
* 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(); // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement prepareCursorControllers(); } 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; if (method instanceof TransformationMethod2) { TransformationMethod2 method2 = (TransformationMethod2) method; mAllowTransformationLengthChange = !mTextIsSelectable && !(mText instanceof Editable); method2.setLengthChangesAllowed(mAllowTransformationLengthChange); } else { mAllowTransformationLengthChange = false; } 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 start padding of the view, plus space for the start * Drawable if any. * * @hide */ public int getCompoundPaddingStart() { resolveDrawables(); switch(getResolvedLayoutDirection()) { default: case LAYOUT_DIRECTION_LTR: return getCompoundPaddingLeft(); case LAYOUT_DIRECTION_RTL: return getCompoundPaddingRight(); } } /** * Returns the end padding of the view, plus space for the end * Drawable if any. * * @hide */ public int getCompoundPaddingEnd() { resolveDrawables(); switch(getResolvedLayoutDirection()) { default: case LAYOUT_DIRECTION_LTR: return getCompoundPaddingRight(); case LAYOUT_DIRECTION_RTL: return getCompoundPaddingLeft(); } } /** * 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 start padding of the view, including the start * Drawable if any. * * @hide */ public int getTotalPaddingStart() { return getCompoundPaddingStart(); } /** * Returns the total end padding of the view, including the end * Drawable if any. * * @hide */ public int getTotalPaddingEnd() { return getCompoundPaddingEnd(); } /** * 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. if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); dr.mDrawableLeft = null; if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); dr.mDrawableTop = null; if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); dr.mDrawableRight = null; if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(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(); } if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) { dr.mDrawableLeft.setCallback(null); } dr.mDrawableLeft = left; if (dr.mDrawableTop != top && dr.mDrawableTop != null) { dr.mDrawableTop.setCallback(null); } dr.mDrawableTop = top; if (dr.mDrawableRight != right && dr.mDrawableRight != null) { dr.mDrawableRight.setCallback(null); } dr.mDrawableRight = right; if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { dr.mDrawableBottom.setCallback(null); } dr.mDrawableBottom = bottom; final Rect compoundRect = dr.mCompoundRect; int[] state; state = getDrawableState(); if (left != null) { left.setState(state); left.copyBounds(compoundRect); left.setCallback(this); dr.mDrawableSizeLeft = compoundRect.width(); dr.mDrawableHeightLeft = compoundRect.height(); } else { dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; } if (right != null) { right.setState(state); right.copyBounds(compoundRect); right.setCallback(this); dr.mDrawableSizeRight = compoundRect.width(); dr.mDrawableHeightRight = compoundRect.height(); } else { dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; } if (top != null) { top.setState(state); top.copyBounds(compoundRect); top.setCallback(this); dr.mDrawableSizeTop = compoundRect.height(); dr.mDrawableWidthTop = compoundRect.width(); } else { dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; } if (bottom != null) { bottom.setState(state); bottom.copyBounds(compoundRect); bottom.setCallback(this); 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); } /** * Sets the Drawables (if any) to appear to the start of, above, * to the end 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_drawableStart * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawableBottom * * @hide */ public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, Drawable bottom) { Drawables dr = mDrawables; final boolean drawables = start != null || top != null || end != 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. if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null); dr.mDrawableStart = null; if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); dr.mDrawableTop = null; if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null); dr.mDrawableEnd = null; if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); dr.mDrawableBottom = null; dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } } else { if (dr == null) { mDrawables = dr = new Drawables(); } if (dr.mDrawableStart != start && dr.mDrawableStart != null) { dr.mDrawableStart.setCallback(null); } dr.mDrawableStart = start; if (dr.mDrawableTop != top && dr.mDrawableTop != null) { dr.mDrawableTop.setCallback(null); } dr.mDrawableTop = top; if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) { dr.mDrawableEnd.setCallback(null); } dr.mDrawableEnd = end; if (dr.mDrawableBottom != bottom && dr.mDrawableBottom != null) { dr.mDrawableBottom.setCallback(null); } dr.mDrawableBottom = bottom; final Rect compoundRect = dr.mCompoundRect; int[] state; state = getDrawableState(); if (start != null) { start.setState(state); start.copyBounds(compoundRect); start.setCallback(this); dr.mDrawableSizeStart = compoundRect.width(); dr.mDrawableHeightStart = compoundRect.height(); } else { dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0; } if (end != null) { end.setState(state); end.copyBounds(compoundRect); end.setCallback(this); dr.mDrawableSizeEnd = compoundRect.width(); dr.mDrawableHeightEnd = compoundRect.height(); } else { dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0; } if (top != null) { top.setState(state); top.copyBounds(compoundRect); top.setCallback(this); dr.mDrawableSizeTop = compoundRect.height(); dr.mDrawableWidthTop = compoundRect.width(); } else { dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0; } if (bottom != null) { bottom.setState(state); bottom.copyBounds(compoundRect); bottom.setCallback(this); dr.mDrawableSizeBottom = compoundRect.height(); dr.mDrawableWidthBottom = compoundRect.width(); } else { dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0; } } resolveDrawables(); invalidate(); requestLayout(); } /** * Sets the Drawables (if any) to appear to the start of, above, * to the end 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 start Resource identifier of the start Drawable. * @param top Resource identifier of the top Drawable. * @param end Resource identifier of the end Drawable. * @param bottom Resource identifier of the bottom Drawable. * * @attr ref android.R.styleable#TextView_drawableStart * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawableBottom * * @hide */ public void setCompoundDrawablesRelativeWithIntrinsicBounds(int start, int top, int end, int bottom) { resetResolvedDrawables(); final Resources resources = getContext().getResources(); setCompoundDrawablesRelativeWithIntrinsicBounds( start != 0 ? resources.getDrawable(start) : null, top != 0 ? resources.getDrawable(top) : null, end != 0 ? resources.getDrawable(end) : null, bottom != 0 ? resources.getDrawable(bottom) : null); } /** * Sets the Drawables (if any) to appear to the start of, above, * to the end 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_drawableStart * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableEnd * @attr ref android.R.styleable#TextView_drawableBottom * * @hide */ public void setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable start, Drawable top, Drawable end, Drawable bottom) { resetResolvedDrawables(); if (start != null) { start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight()); } if (end != null) { end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight()); } if (top != null) { top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight()); } if (bottom != null) { bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight()); } setCompoundDrawablesRelative(start, top, end, 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 }; } } /** * Returns drawables for the start, top, end, and bottom borders. * * @hide */ public Drawable[] getCompoundDrawablesRelative() { final Drawables dr = mDrawables; if (dr != null) { return new Drawable[] { dr.mDrawableStart, dr.mDrawableTop, dr.mDrawableEnd, 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 != mPaddingLeft || right != mPaddingRight || top != mPaddingTop || bottom != mPaddingBottom) { 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); if (appearance.getBoolean(com.android.internal.R.styleable.TextAppearance_textAllCaps, false)) { setTransformationMethod(new AllCapsTransformationMethod(getContext())); } 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()) { mUserSetTextScaleX = true; 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; } /** *
Return the current color selected for normal text.
* * @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(); } /** *Return the color used to paint the hint text.
* * @return Returns the list of hint text colors. */ public final ColorStateList getHintTextColors() { return mHintTextColor; } /** *Return the current color selected to paint the hint text.
* * @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(); } /** *Returns the color used to paint links in the text.
* * @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.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { gravity |= Gravity.START; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { gravity |= Gravity.TOP; } boolean newLayout = false; if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != (mGravity & Gravity.RELATIVE_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. * * Setting this value overrides any other (minimum) height setting. A single line TextView will * set this value to 1. * * @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. * * Setting this value overrides any other (minimum) number of lines setting. * * @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. * * Setting this value overrides any other (maximum) height setting. * * @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. This option is mutually exclusive with the * {@link #setMaxLines(int)} method. * * Setting this value overrides any other (maximum) number of lines setting. * * @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. * * Note that setting this value overrides any other (minimum / maximum) number of lines or * height setting. A single line TextView will set this value to 1. * * @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. * * Note that setting this value overrides any other (minimum / maximum) number of lines or * height setting. * * @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 bymult
and have add
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);
}
if (dr.mDrawableStart != null && dr.mDrawableStart.isStateful()) {
dr.mDrawableStart.setState(state);
}
if (dr.mDrawableEnd != null && dr.mDrawableEnd.isStateful()) {
dr.mDrawableEnd.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;
CharSequence error;
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);
if (error == null) {
out.writeInt(0);
} else {
out.writeInt(1);
TextUtils.writeToParcel(error, 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 + "}";
}
@SuppressWarnings("hiding")
public static final Parcelable.CreatorFor backwards compatibility, if no IME options have been set and the
* text view would not normally advance focus on enter, then
* the NEXT and DONE actions received here will be turned into an enter
* key down/up pair to go through the normal key handling.
*
* @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;
}
}
// This is the handling for some default action.
// Note that for backwards compatibility we don't do this
// default handling if explicit ime options have not been given,
// instead turning this into the normal enter key codes that an
// app may be expecting.
if (actionCode == EditorInfo.IME_ACTION_NEXT) {
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;
} else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
View v = focusSearch(FOCUS_UP);
if (v != null) {
if (!v.requestFocus(FOCUS_UP)) {
throw new IllegalStateException("focus search returned a view " +
"that wasn't able to take focus!");
}
}
return;
} else if (actionCode == EditorInfo.IME_ACTION_DONE) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && imm.isActive(this)) {
imm.hideSoftInputFromWindow(getWindowToken(), 0);
}
return;
}
}
Handler h = getHandler();
if (h != null) {
long eventTime = SystemClock.uptimeMillis();
h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
| KeyEvent.FLAG_EDITOR_ACTION)));
h.sendMessage(h.obtainMessage(ViewRootImpl.DISPATCH_KEY_FROM_IME,
new KeyEvent(SystemClock.uptimeMillis(), eventTime,
KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
| KeyEvent.FLAG_EDITOR_ACTION)));
}
}
/**
* 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 <input-extras>} 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)
* @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
* 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 ArrayListnull
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
* error
is null
, 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 error
is null
, the error message will
* be cleared (and you should provide a null
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) {
switch (getResolvedLayoutDirection()) {
default:
case LAYOUT_DIRECTION_LTR:
setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
dr.mDrawableBottom);
break;
case LAYOUT_DIRECTION_RTL:
setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
dr.mDrawableBottom);
break;
}
} 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);
final float scale = getResources().getDisplayMetrics().density;
mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
mPopup.setFocusable(false);
// The user is entering text, so the input method is needed. We
// don't want the popup to be displayed on top of it.
mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
}
TextView tv = (TextView) mPopup.getContentView();
chooseSize(mPopup, mError, tv);
tv.setText(mError);
mPopup.showAsDropDown(this, getErrorX(), getErrorY());
mPopup.fixDirection(mPopup.isAboveAnchor());
}
private static class ErrorPopup extends PopupWindow {
private boolean mAbove = false;
private final TextView mView;
private int mPopupInlineErrorBackgroundId = 0;
private int mPopupInlineErrorAboveBackgroundId = 0;
ErrorPopup(TextView v, int width, int height) {
super(v, width, height);
mView = v;
// Make sure the TextView has a background set as it will be used the first time it is
// shown and positionned. Initialized with below background, which should have
// dimensions identical to the above version for this to work (and is more likely).
mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
com.android.internal.R.styleable.Theme_errorMessageBackground);
mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
}
void fixDirection(boolean above) {
mAbove = above;
if (above) {
mPopupInlineErrorAboveBackgroundId =
getResourceId(mPopupInlineErrorAboveBackgroundId,
com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
} else {
mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
com.android.internal.R.styleable.Theme_errorMessageBackground);
}
mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
mPopupInlineErrorBackgroundId);
}
private int getResourceId(int currentId, int index) {
if (currentId == 0) {
TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
R.styleable.Theme);
currentId = styledAttributes.getResourceId(index, 0);
styledAttributes.recycle();
}
return currentId;
}
@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) {
fixDirection(above);
}
}
}
/**
* 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 float scale = getResources().getDisplayMetrics().density;
final Drawables dr = mDrawables;
return getWidth() - mPopup.getWidth() - getPaddingRight() -
(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
}
/**
* 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.
*/
final int compoundPaddingTop = getCompoundPaddingTop();
int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
final Drawables dr = mDrawables;
int icontop = compoundPaddingTop +
(vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
/*
* The "2" is the distance between the point and the top edge
* of the background.
*/
final float scale = getResources().getDisplayMetrics().density;
return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
(int) (2 * scale + 0.5f);
}
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(),
mPopup.getWidth(), mPopup.getHeight());
}
restartMarqueeIfNeeded();
return result;
}
private void restartMarqueeIfNeeded() {
if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
mRestartMarquee = false;
startMarquee();
}
}
/**
* 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 {
final int horizontalPadding = getCompoundPaddingLeft();
final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
if (mCursorCount == 0) {
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.0f;
mHighlightPath.computeBounds(sTempRect, false);
invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick),
(int) FloatMath.floor(verticalPadding + sTempRect.top - thick),
(int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick),
(int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick));
}
} else {
for (int i = 0; i < mCursorCount; i++) {
Rect bounds = mCursorDrawable[i].getBounds();
invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
}
}
}
}
private void invalidateCursor() {
int where = getSelectionEnd();
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);
final int horizontalPadding = getCompoundPaddingLeft();
final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
// If used, the cursor drawables can have an arbitrary dimension that can go beyond
// the invalidated lines specified above.
for (int i = 0; i < mCursorCount; i++) {
Rect bounds = mCursorDrawable[i].getBounds();
top = Math.min(top, bounds.top);
bottom = Math.max(bottom, bounds.bottom);
// Horizontal bounds are already full width, no need to update
}
invalidate(horizontalPadding + mScrollX, top + verticalPadding,
horizontalPadding + mScrollX + getWidth() -
getCompoundPaddingLeft() - getCompoundPaddingRight(),
bottom + verticalPadding);
}
}
}
private void registerForPreDraw() {
final ViewTreeObserver observer = getViewTreeObserver();
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) {
/* This code also provides auto-scrolling when a cursor is moved using a
* CursorController (insertion point or selection limits).
* For selection, ensure start or end is visible depending on controller's state.
*/
int curs = getSelectionEnd();
// Do not create the controller if it is not already created.
if (mSelectionModifierCursorController != null &&
mSelectionModifierCursorController.isSelectionStartDragged()) {
curs = getSelectionStart();
}
/*
* 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();
}
// This has to be checked here since:
// - onFocusChanged cannot start it when focus is given to a view with selected text (after
// a screen rotation) since layout is not yet initialized at that point.
if (mCreatedWithASelection) {
startSelectionActionMode();
mCreatedWithASelection = false;
}
// Phone specific code (there is no ExtractEditText on tablets).
// ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
// not be set. Do the test here instead.
if (this instanceof ExtractEditText && hasSelection()) {
startSelectionActionMode();
}
mPreDrawState = PREDRAW_DONE;
return !changed;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mTemporaryDetach = false;
if (mShowErrorAfterAttach) {
showError();
mShowErrorAfterAttach = false;
}
final ViewTreeObserver observer = getViewTreeObserver();
// No need to create the controller.
// The get method will add the listener on controller creation.
if (mInsertionPointCursorController != null) {
observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
}
if (mSelectionModifierCursorController != null) {
observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
}
// Resolve drawables as the layout direction has been resolved
resolveDrawables();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
final ViewTreeObserver observer = getViewTreeObserver();
if (mPreDrawState != PREDRAW_NOT_REGISTERED) {
observer.removeOnPreDrawListener(this);
mPreDrawState = PREDRAW_NOT_REGISTERED;
}
if (mError != null) {
hideError();
}
if (mBlink != null) {
mBlink.removeCallbacks(mBlink);
}
if (mInsertionPointCursorController != null) {
mInsertionPointCursorController.onDetached();
}
if (mSelectionModifierCursorController != null) {
mSelectionModifierCursorController.onDetached();
}
hideControllers();
resetResolvedDrawables();
}
@Override
protected boolean isPaddingOffsetRequired() {
return mShadowRadius != 0 || mDrawables != null;
}
@Override
protected int getLeftPaddingOffset() {
return getCompoundPaddingLeft() - mPaddingLeft +
(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 -(getCompoundPaddingRight() - mPaddingRight) +
(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 ||
who == mDrawables.mDrawableStart || who == mDrawables.mDrawableEnd;
}
return verified;
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (mDrawables != null) {
if (mDrawables.mDrawableLeft != null) {
mDrawables.mDrawableLeft.jumpToCurrentState();
}
if (mDrawables.mDrawableTop != null) {
mDrawables.mDrawableTop.jumpToCurrentState();
}
if (mDrawables.mDrawableRight != null) {
mDrawables.mDrawableRight.jumpToCurrentState();
}
if (mDrawables.mDrawableBottom != null) {
mDrawables.mDrawableBottom.jumpToCurrentState();
}
if (mDrawables.mDrawableStart != null) {
mDrawables.mDrawableStart.jumpToCurrentState();
}
if (mDrawables.mDrawableEnd != null) {
mDrawables.mDrawableEnd.jumpToCurrentState();
}
}
}
@Override
public void invalidateDrawable(Drawable drawable) {
if (verifyDrawable(drawable)) {
final Rect dirty = drawable.getBounds();
int scrollX = mScrollX;
int scrollY = mScrollY;
// IMPORTANT: The coordinates below are based on the coordinates computed
// for each compound drawable in onDraw(). Make sure to update each section
// accordingly.
final TextView.Drawables drawables = mDrawables;
if (drawables != null) {
if (drawable == drawables.mDrawableLeft) {
final int compoundPaddingTop = getCompoundPaddingTop();
final int compoundPaddingBottom = getCompoundPaddingBottom();
final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
scrollX += mPaddingLeft;
scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
} else if (drawable == drawables.mDrawableRight) {
final int compoundPaddingTop = getCompoundPaddingTop();
final int compoundPaddingBottom = getCompoundPaddingBottom();
final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
} else if (drawable == drawables.mDrawableTop) {
final int compoundPaddingLeft = getCompoundPaddingLeft();
final int compoundPaddingRight = getCompoundPaddingRight();
final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
scrollY += mPaddingTop;
} else if (drawable == drawables.mDrawableBottom) {
final int compoundPaddingLeft = getCompoundPaddingLeft();
final int compoundPaddingRight = getCompoundPaddingRight();
final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
}
}
invalidate(dirty.left + scrollX, dirty.top + scrollY,
dirty.right + scrollX, dirty.bottom + scrollY);
}
}
/**
* @hide
*/
@Override
public int getResolvedLayoutDirection(Drawable who) {
if (who == null) return View.LAYOUT_DIRECTION_LTR;
if (mDrawables != null) {
final Drawables drawables = mDrawables;
if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight ||
who == drawables.mDrawableTop || who == drawables.mDrawableBottom ||
who == drawables.mDrawableStart || who == drawables.mDrawableEnd) {
return getResolvedLayoutDirection();
}
}
return super.getResolvedLayoutDirection(who);
}
@Override
protected boolean onSetAlpha(int alpha) {
// Alpha is supported if and only if the drawing can be done in one pass.
// TODO text with spans with a background color currently do not respect this alpha.
if (getBackground() == null) {
mCurrentAlpha = alpha;
final Drawables dr = mDrawables;
if (dr != null) {
if (dr.mDrawableLeft != null) dr.mDrawableLeft.mutate().setAlpha(alpha);
if (dr.mDrawableTop != null) dr.mDrawableTop.mutate().setAlpha(alpha);
if (dr.mDrawableRight != null) dr.mDrawableRight.mutate().setAlpha(alpha);
if (dr.mDrawableBottom != null) dr.mDrawableBottom.mutate().setAlpha(alpha);
if (dr.mDrawableStart != null) dr.mDrawableStart.mutate().setAlpha(alpha);
if (dr.mDrawableEnd != null) dr.mDrawableEnd.mutate().setAlpha(alpha);
}
return true;
}
mCurrentAlpha = 255;
return false;
}
/**
* When a TextView is used to display a useful piece of information to the user (such as a
* contact's address), it should be made selectable, so that the user can select and copy this
* content.
*
* Use {@link #setTextIsSelectable(boolean)} or the
* {@link android.R.styleable#TextView_textIsSelectable} XML attribute to make this TextView
* selectable (text is not selectable by default).
*
* Note that this method simply returns the state of this flag. Although this flag has to be set
* in order to select text in non-editable TextView, the content of an {@link EditText} can
* always be selected, independently of the value of this flag.
*
* @return True if the text displayed in this TextView can be selected by the user.
*
* @attr ref android.R.styleable#TextView_textIsSelectable
*/
public boolean isTextSelectable() {
return mTextIsSelectable;
}
/**
* Sets whether or not (default) the content of this view is selectable by the user.
*
* Note that this methods affect the {@link #setFocusable(boolean)},
* {@link #setFocusableInTouchMode(boolean)} {@link #setClickable(boolean)} and
* {@link #setLongClickable(boolean)} states and you may want to restore these if they were
* customized.
*
* See {@link #isTextSelectable} for details.
*
* @param selectable Whether or not the content of this TextView should be selectable.
*/
public void setTextIsSelectable(boolean selectable) {
if (mTextIsSelectable == selectable) return;
mTextIsSelectable = selectable;
setFocusableInTouchMode(selectable);
setFocusable(selectable);
setClickable(selectable);
setLongClickable(selectable);
// mInputType is already EditorInfo.TYPE_NULL and mInput is null;
setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
// Called by setText above, but safer in case of future code changes
prepareCursorControllers();
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState;
if (mSingleLine) {
drawableState = super.onCreateDrawableState(extraSpace);
} else {
drawableState = super.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
}
if (mTextIsSelectable) {
// Disable pressed state, which was introduced when TextView was made clickable.
// Prevents text color change.
// setClickable(false) would have a similar effect, but it also disables focus changes
// and long press actions, which are both needed by text selection.
final int length = drawableState.length;
for (int i = 0; i < length; i++) {
if (drawableState[i] == R.attr.state_pressed) {
final int[] nonPressedState = new int[length - 1];
System.arraycopy(drawableState, 0, nonPressedState, 0, i);
System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
return nonPressedState;
}
}
}
return drawableState;
}
@Override
protected void onDraw(Canvas canvas) {
if (mPreDrawState == PREDRAW_DONE) {
final ViewTreeObserver observer = getViewTreeObserver();
observer.removeOnPreDrawListener(this);
mPreDrawState = PREDRAW_NOT_REGISTERED;
}
if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return;
restartMarqueeIfNeeded();
// 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;
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mDrawableLeft != null) {
canvas.save();
canvas.translate(scrollX + mPaddingLeft,
scrollY + compoundPaddingTop +
(vspace - dr.mDrawableHeightLeft) / 2);
dr.mDrawableLeft.draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
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();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
if (dr.mDrawableTop != null) {
canvas.save();
canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2,
scrollY + mPaddingTop);
dr.mDrawableTop.draw(canvas);
canvas.restore();
}
// IMPORTANT: The coordinates computed are also used in invalidateDrawable()
// Make sure to update invalidateDrawable() when changing this code.
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();
}
}
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);
if (mCurrentAlpha != 255) {
// If set, the alpha will override the color's alpha. Multiply the alphas.
mTextPaint.setAlpha((mCurrentAlpha * Color.alpha(color)) / 255);
}
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);
}
final int layoutDirection = getResolvedLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (!mSingleLine && getLineCount() == 1 && canMarquee() &&
(absoluteGravity & 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;
boolean drawCursor = false;
// 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 = getSelectionStart();
selEnd = getSelectionEnd();
if (selStart >= 0) {
if (mHighlightPath == null)
mHighlightPath = new Path();
if (selStart == selEnd) {
if (isCursorVisible() &&
(SystemClock.uptimeMillis() - mShowCursor) % (2 * BLINK) < BLINK) {
if (mHighlightPathBogus) {
mHighlightPath.reset();
mLayout.getCursorPath(selStart, mHighlightPath, mText);
updateCursorsPositions();
mHighlightPathBogus = false;
}
// XXX should pass to skin instead of drawing directly
mHighlightPaint.setColor(cursorcolor);
if (mCurrentAlpha != 255) {
mHighlightPaint.setAlpha(
(mCurrentAlpha * Color.alpha(cursorcolor)) / 255);
}
mHighlightPaint.setStyle(Paint.Style.STROKE);
highlight = mHighlightPath;
drawCursor = mCursorCount > 0;
}
} else if (textCanBeSelected()) {
if (mHighlightPathBogus) {
mHighlightPath.reset();
mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
mHighlightPathBogus = false;
}
// XXX should pass to skin instead of drawing directly
mHighlightPaint.setColor(mHighlightColor);
if (mCurrentAlpha != 255) {
mHighlightPaint.setAlpha(
(mCurrentAlpha * Color.alpha(mHighlightColor)) / 255);
}
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;
final int cursorOffsetVertical = voffsetCursor - voffsetText;
if (ims != null && ims.mBatchEditNesting == 0) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) {
if (imm.isActive(this)) {
boolean reported = false;
if (ims.mContentChanged || ims.mSelectionModeChanged) {
// 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, cursorOffsetVertical);
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);
}
}
}
if (mCorrectionHighlighter != null) {
mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
}
if (drawCursor) {
drawCursor(canvas, cursorOffsetVertical);
// Rely on the drawable entirely, do not draw the cursor line.
// Has to be done after the IMM related code above which relies on the highlight.
highlight = null;
}
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
/* Comment out until we decide what to do about animations
if (currentTransformation != null) {
mTextPaint.setLinearTextOn(isLinearTextOn);
}
*/
canvas.restore();
}
private void updateCursorsPositions() {
if (mCursorDrawableRes == 0) {
mCursorCount = 0;
return;
}
final int offset = getSelectionStart();
final int line = mLayout.getLineForOffset(offset);
final int top = mLayout.getLineTop(line);
final int bottom = mLayout.getLineTop(line + 1);
mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
int middle = bottom;
if (mCursorCount == 2) {
// Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
middle = (top + bottom) >> 1;
}
updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
if (mCursorCount == 2) {
updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
}
}
private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
if (mCursorDrawable[cursorIndex] == null)
mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
if (mTempRect == null) mTempRect = new Rect();
mCursorDrawable[cursorIndex].getPadding(mTempRect);
final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
horizontal = Math.max(0.5f, horizontal - 0.5f);
final int left = (int) (horizontal) - mTempRect.left;
mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
bottom + mTempRect.bottom);
}
private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
final boolean translate = cursorOffsetVertical != 0;
if (translate) canvas.translate(0, cursorOffsetVertical);
for (int i = 0; i < mCursorCount; i++) {
mCursorDrawable[i].draw(canvas);
}
if (translate) canvas.translate(0, -cursorOffsetVertical);
}
@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 = KeyEvent.changeAction(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 = KeyEvent.changeAction(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);
}
hideErrorIfUnchanged();
} 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.
*/
private 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;
}
/**
* Returns true if pressing TAB in this field advances focus instead
* of inserting the character. Insert tabs only in multi-line editors.
*/
private boolean shouldAdvanceFocusOnTab() {
if (mInput != null && !mSingleLine) {
if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION;
if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
|| variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
return false;
}
}
}
return true;
}
private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
if (!isEnabled()) {
return 0;
}
switch (keyCode) {
case KeyEvent.KEYCODE_ENTER:
mEnterKeyIsDown = true;
if (event.hasNoModifiers()) {
// When mInputContentType is set, we know that we are
// running in a "modern" cupcake environment, so don't need
// to worry about the application trying to capture
// enter key events.
if (mInputContentType != null) {
// If there is an action listener, given them a
// chance to consume the event.
if (mInputContentType.onEditorActionListener != null &&
mInputContentType.onEditorActionListener.onEditorAction(
this, EditorInfo.IME_NULL, event)) {
mInputContentType.enterDown = true;
// We are consuming the enter key for them.
return -1;
}
}
// If our editor should move focus when enter is pressed, or
// this is a generated event from an IME action button, then
// don't let it be inserted into the text.
if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
|| shouldAdvanceFocusOnEnter()) {
if (mOnClickListener != null) {
return 0;
}
return -1;
}
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
mDPadCenterIsDown = true;
if (event.hasNoModifiers()) {
if (shouldAdvanceFocusOnEnter()) {
return 0;
}
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
if (shouldAdvanceFocusOnTab()) {
return 0;
}
}
break;
// Has to be done on key down (and not on key up) to correctly be intercepted.
case KeyEvent.KEYCODE_BACK:
if (mSelectionActionMode != null) {
stopSelectionActionMode();
return -1;
}
break;
}
if (mInput != null) {
resetErrorChangedFlag();
boolean doDown = true;
if (otherEvent != null) {
try {
beginBatchEdit();
final boolean handled = mInput.onKeyOther(this, (Editable) mText, otherEvent);
hideErrorIfUnchanged();
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();
final boolean handled = mInput.onKeyDown(this, (Editable) mText, keyCode, event);
endBatchEdit();
hideErrorIfUnchanged();
if (handled) return 1;
}
}
// 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;
}
/**
* Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
* can be recorded.
* @hide
*/
public void resetErrorChangedFlag() {
/*
* 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;
}
/**
* @hide
*/
public void hideErrorIfUnchanged() {
if (mError != null && !mErrorWasChanged) {
setError(null, null);
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (!isEnabled()) {
return super.onKeyUp(keyCode, event);
}
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
mDPadCenterIsDown = false;
if (event.hasNoModifiers()) {
/*
* 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.peekInstance();
if (imm != null) {
imm.viewClicked(this);
imm.showSoftInput(this, 0);
}
}
}
}
return super.onKeyUp(keyCode, event);
case KeyEvent.KEYCODE_ENTER:
mEnterKeyIsDown = false;
if (event.hasNoModifiers()) {
if (mInputContentType != null
&& mInputContentType.onEditorActionListener != null
&& mInputContentType.enterDown) {
mInputContentType.enterDown = false;
if (mInputContentType.onEditorActionListener.onEditorAction(
this, EditorInfo.IME_NULL, event)) {
return true;
}
}
if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
|| 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;
} else if ((event.getFlags()
& KeyEvent.FLAG_EDITOR_ACTION) != 0) {
// No target for next focus, but make sure the IME
// if this came from it.
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && imm.isActive(this)) {
imm.hideSoftInputFromWindow(getWindowToken(), 0);
}
}
}
}
return super.onKeyUp(keyCode, event);
}
break;
}
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() && isEnabled()) {
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_NULL;
}
if (focusSearch(FOCUS_DOWN) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
}
if (focusSearch(FOCUS_UP) != null) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
}
if ((outAttrs.imeOptions&EditorInfo.IME_MASK_ACTION)
== EditorInfo.IME_ACTION_UNSPECIFIED) {
if ((outAttrs.imeOptions&EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
// 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;
} else {
// An action has not been set, and there is no focus to move
// to, so let's just supply a "done" action.
outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
}
if (!shouldAdvanceFocusOnEnter()) {
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
}
if (isMultilineInputType(outAttrs.inputType)) {
// Multi-line text editors should always show an enter key.
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
outAttrs.hintText = mHint;
if (mText instanceof Editable) {
InputConnection ic = new EditableInputConnection(this);
outAttrs.initialSelStart = getSelectionStart();
outAttrs.initialSelEnd = getSelectionEnd();
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 request in to outText.
* @return Returns true if the text was successfully extracted, else false.
*/
public boolean extractText(ExtractedTextRequest request,
ExtractedText outText) {
return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
EXTRACT_UNKNOWN, outText);
}
static final int EXTRACT_NOTHING = -2;
static final int EXTRACT_UNKNOWN = -1;
boolean extractTextInternal(ExtractedTextRequest request,
int partialStartOffset, int partialEndOffset, int delta,
ExtractedText outText) {
final CharSequence content = mText;
if (content != null) {
if (partialStartOffset != EXTRACT_NOTHING) {
final int N = content.length();
if (partialStartOffset < 0) {
outText.partialStartOffset = outText.partialEndOffset = -1;
partialStartOffset = 0;
partialEndOffset = N;
} else {
// Now use the delta to determine the actual amount of text
// we need.
partialEndOffset += delta;
// 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 - delta;
if (partialStartOffset > N) {
partialStartOffset = N;
} else if (partialStartOffset < 0) {
partialStartOffset = 0;
}
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);
}
} else {
outText.partialStartOffset = 0;
outText.partialEndOffset = 0;
outText.text = "";
}
outText.flags = 0;
if (MetaKeyKeyListener.getMetaState(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
outText.flags |= ExtractedText.FLAG_SELECTING;
}
if (mSingleLine) {
outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
}
outText.startOffset = 0;
outText.selectionStart = getSelectionStart();
outText.selectionEnd = getSelectionEnd();
return true;
}
return false;
}
boolean reportExtractedText() {
final InputMethodState ims = mInputMethodState;
if (ims != null) {
final boolean contentChanged = ims.mContentChanged;
if (contentChanged || ims.mSelectionModeChanged) {
ims.mContentChanged = false;
ims.mSelectionModeChanged = false;
final ExtractedTextRequest req = mInputMethodState.mExtracting;
if (req != null) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
+ ims.mChangedStart + " end=" + ims.mChangedEnd
+ " delta=" + ims.mChangedDelta);
if (ims.mChangedStart < 0 && !contentChanged) {
ims.mChangedStart = EXTRACT_NOTHING;
}
if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
ims.mChangedDelta, ims.mTmpExtracted)) {
if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
+ ims.mTmpExtracted.partialStartOffset
+ " end=" + ims.mTmpExtracted.partialEndOffset
+ ": " + ims.mTmpExtracted.text);
imm.updateExtractedText(this, req.token,
mInputMethodState.mTmpExtracted);
ims.mChangedStart = EXTRACT_UNKNOWN;
ims.mChangedEnd = EXTRACT_UNKNOWN;
ims.mChangedDelta = 0;
ims.mContentChanged = false;
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 (text.text != null) {
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);
// Finally, update the selection mode.
if ((text.flags&ExtractedText.FLAG_SELECTING) != 0) {
MetaKeyKeyListener.startSelecting(this, sp);
} else {
MetaKeyKeyListener.stopSelecting(this, sp);
}
}
/**
* @hide
*/
public void setExtracting(ExtractedTextRequest req) {
if (mInputMethodState != null) {
mInputMethodState.mExtracting = req;
}
// This stops a possible text selection mode. Maybe not intended.
hideControllers();
}
/**
* 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) {
// intentionally empty
}
/**
* Called by the framework in response to a text auto-correction (such as fixing a typo using a
* a dictionnary) from the current input method, provided by it calling
* {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
* implementation flashes the background of the corrected word to provide feedback to the user.
*
* @param info The auto correct info about the text that was corrected.
*/
public void onCommitCorrection(CorrectionInfo info) {
if (mCorrectionHighlighter == null) {
mCorrectionHighlighter = new CorrectionHighlighter();
} else {
mCorrectionHighlighter.invalidate(false);
}
mCorrectionHighlighter.highlight(info);
}
private class CorrectionHighlighter {
private final Path mPath = new Path();
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int mStart, mEnd;
private long mFadingStartTime;
private final static int FADE_OUT_DURATION = 400;
public CorrectionHighlighter() {
mPaint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
mPaint.setStyle(Paint.Style.FILL);
}
public void highlight(CorrectionInfo info) {
mStart = info.getOffset();
mEnd = mStart + info.getNewText().length();
mFadingStartTime = SystemClock.uptimeMillis();
if (mStart < 0 || mEnd < 0) {
stopAnimation();
}
}
public void draw(Canvas canvas, int cursorOffsetVertical) {
if (updatePath() && updatePaint()) {
if (cursorOffsetVertical != 0) {
canvas.translate(0, cursorOffsetVertical);
}
canvas.drawPath(mPath, mPaint);
if (cursorOffsetVertical != 0) {
canvas.translate(0, -cursorOffsetVertical);
}
invalidate(true);
} else {
stopAnimation();
invalidate(false);
}
}
private boolean updatePaint() {
final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
if (duration > FADE_OUT_DURATION) return false;
final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
final int highlightColorAlpha = Color.alpha(mHighlightColor);
final int color = (mHighlightColor & 0x00FFFFFF) +
((int) (highlightColorAlpha * coef) << 24);
mPaint.setColor(color);
return true;
}
private boolean updatePath() {
final Layout layout = TextView.this.mLayout;
if (layout == null) return false;
// Update in case text is edited while the animation is run
final int length = mText.length();
int start = Math.min(length, mStart);
int end = Math.min(length, mEnd);
mPath.reset();
TextView.this.mLayout.getSelectionPath(start, end, mPath);
return true;
}
private void invalidate(boolean delayed) {
if (TextView.this.mLayout == null) return;
synchronized (sTempRect) {
mPath.computeBounds(sTempRect, false);
int left = getCompoundPaddingLeft();
int top = getExtendedPaddingTop() + getVerticalOffset(true);
if (delayed) {
TextView.this.postInvalidateDelayed(16, // 60 Hz update
left + (int) sTempRect.left, top + (int) sTempRect.top,
left + (int) sTempRect.right, top + (int) sTempRect.bottom);
} else {
TextView.this.postInvalidate((int) sTempRect.left, (int) sTempRect.top,
(int) sTempRect.right, (int) sTempRect.bottom);
}
}
}
private void stopAnimation() {
TextView.this.mCorrectionHighlighter = null;
}
}
public void beginBatchEdit() {
mInBatchEditControllers = true;
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 = EXTRACT_UNKNOWN;
ims.mChangedEnd = EXTRACT_UNKNOWN;
ims.mContentChanged = false;
}
onBeginBatchEdit();
}
}
}
public void endBatchEdit() {
mInBatchEditControllers = false;
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 || ims.mSelectionModeChanged) {
updateAfterEdit();
reportExtractedText();
} else if (ims.mCursorChanged) {
// Cheezy way to get us to report the current cursor location.
invalidateCursor();
}
}
void updateAfterEdit() {
invalidate();
int curs = getSelectionStart();
if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
registerForPreDraw();
}
if (curs >= 0) {
mHighlightPathBogus = true;
makeBlink();
bringPointIntoView(curs);
}
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() {
// intentionally empty
}
/**
* 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() {
// intentionally empty
}
/**
* 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;
// Since it depends on the value of mLayout
prepareCursorControllers();
}
/**
* 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);
}
@Override
protected void resetResolvedLayoutDirection() {
super.resetResolvedLayoutDirection();
if (mLayoutAlignment != null &&
(mTextAlign == TextAlign.VIEW_START ||
mTextAlign == TextAlign.VIEW_END)) {
mLayoutAlignment = null;
}
}
private Layout.Alignment getLayoutAlignment() {
if (mLayoutAlignment == null) {
Layout.Alignment alignment;
TextAlign textAlign = mTextAlign;
switch (textAlign) {
case INHERIT:
// fall through to gravity temporarily
// intention is to inherit value through view hierarchy.
case GRAVITY:
switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
case Gravity.START:
alignment = Layout.Alignment.ALIGN_NORMAL;
break;
case Gravity.END:
alignment = Layout.Alignment.ALIGN_OPPOSITE;
break;
case Gravity.LEFT:
alignment = Layout.Alignment.ALIGN_LEFT;
break;
case Gravity.RIGHT:
alignment = Layout.Alignment.ALIGN_RIGHT;
break;
case Gravity.CENTER_HORIZONTAL:
alignment = Layout.Alignment.ALIGN_CENTER;
break;
default:
alignment = Layout.Alignment.ALIGN_NORMAL;
break;
}
break;
case TEXT_START:
alignment = Layout.Alignment.ALIGN_NORMAL;
break;
case TEXT_END:
alignment = Layout.Alignment.ALIGN_OPPOSITE;
break;
case CENTER:
alignment = Layout.Alignment.ALIGN_CENTER;
break;
case VIEW_START:
alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
break;
case VIEW_END:
alignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
break;
default:
alignment = Layout.Alignment.ALIGN_NORMAL;
break;
}
mLayoutAlignment = alignment;
}
return mLayoutAlignment;
}
/**
* 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 = getLayoutAlignment();
boolean shouldEllipsize = mEllipsize != null && mInput == null;
if (mTextDir == null) {
resolveTextDirection();
}
if (mText instanceof Spannable) {
mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w,
alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null,
ellipsisWidth);
} else {
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, 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);
}
mSavedLayout = (BoringLayout) mLayout;
} else if (shouldEllipsize && 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 (shouldEllipsize) {
mLayout = new StaticLayout(mTransformed,
0, mTransformed.length(),
mTextPaint, w, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mEllipsize,
ellipsisWidth);
} else {
mLayout = new StaticLayout(mTransformed, mTextPaint,
w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
} else if (shouldEllipsize) {
mLayout = new StaticLayout(mTransformed,
0, mTransformed.length(),
mTextPaint, w, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mEllipsize,
ellipsisWidth);
} else {
mLayout = new StaticLayout(mTransformed, mTextPaint,
w, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
}
shouldEllipsize = mEllipsize != null;
mHintLayout = null;
if (mHint != null) {
if (shouldEllipsize) hintWidth = w;
if (hintBoring == UNKNOWN_BORING) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}
if (hintBoring != null) {
if (hintBoring.width <= hintWidth &&
(!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
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 if (shouldEllipsize && hintBoring.width <= hintWidth) {
if (mSavedHintLayout != null) {
mHintLayout = mSavedHintLayout.
replaceOrMake(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad, mEllipsize,
ellipsisWidth);
} else {
mHintLayout = BoringLayout.make(mHint, mTextPaint,
hintWidth, alignment, mSpacingMult, mSpacingAdd,
hintBoring, mIncludePad, mEllipsize,
ellipsisWidth);
}
} else if (shouldEllipsize) {
mHintLayout = new StaticLayout(mHint,
0, mHint.length(),
mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mEllipsize,
ellipsisWidth);
} else {
mHintLayout = new StaticLayout(mHint, mTextPaint,
hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
} else if (shouldEllipsize) {
mHintLayout = new StaticLayout(mHint,
0, mHint.length(),
mTextPaint, hintWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, mEllipsize,
ellipsisWidth);
} else {
mHintLayout = new StaticLayout(mHint, mTextPaint,
hintWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
}
if (bringIntoView) {
registerForPreDraw();
}
if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (!compressText(ellipsisWidth)) {
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.MATCH_PARENT) {
startMarquee();
} else {
// Defer the start of the marquee until we know our width (see setFrame())
mRestartMarquee = true;
}
}
}
// CursorControllers need a non-null mLayout
prepareCursorControllers();
}
private boolean compressText(float width) {
if (isHardwareAccelerated()) return false;
// Only compress the text if it hasn't been compressed by the previous pass
if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX &&
mTextPaint.getTextScaleX() == 1.0f) {
final float textWidth = mLayout.getLineWidth(0);
final float overflow = (textWidth + 1.0f - width) / width;
if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
post(new Runnable() {
public void run() {
requestLayout();
}
});
return true;
}
}
return false;
}
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;
if (mTextDir == null) {
resolveTextDirection();
}
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, mTextDir, 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 && mEllipsize == 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;
if (mHorizontallyScrolling) want = VERY_WIDE;
int hintWant = want;
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, heightSize);
}
}
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, mEllipsize != null));
}
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;
layout.setMaximumVisibleLineCount(0);
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) {
layout.setMaximumVisibleLineCount(mMaximum);
desired = layout.getLineTop(mMaximum);
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.MATCH_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);
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT &&
mLayoutParams.height != LayoutParams.MATCH_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;
// Convert to left, center, or right alignment.
if (a == Layout.Alignment.ALIGN_NORMAL) {
a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_LEFT :
Layout.Alignment.ALIGN_RIGHT;
} else if (a == Layout.Alignment.ALIGN_OPPOSITE){
a = dir == Layout.DIR_LEFT_TO_RIGHT ? Layout.Alignment.ALIGN_RIGHT :
Layout.Alignment.ALIGN_LEFT;
}
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_RIGHT) {
int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
scrollx = right - hspace;
} else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
scrollx = (int) FloatMath.floor(mLayout.getLineLeft(line));
}
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;
if (mLayout == null) return changed;
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_LEFT:
grav = 1;
break;
case ALIGN_RIGHT:
grav = -1;
break;
case ALIGN_NORMAL:
grav = mLayout.getParagraphDirection(line);
break;
case ALIGN_OPPOSITE:
grav = -mLayout.getParagraphDirection(line);
break;
case ALIGN_CENTER:
default:
grav = 0;
break;
}
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);
awakenScrollBars(mScroller.getDuration());
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.
if (mTempRect == null) mTempRect = new Rect();
mTempRect.set(x, top, x + 1, bottom);
getInterestingRect(mTempRect, line);
mTempRect.offset(mScrollX, mScrollY);
if (requestRectangleOnScreen(mTempRect)) {
changed = true;
}
}
return changed;
}
/**
* Move the cursor, if needed, so that it is at an offset that is visible
* to the user. This will not move the cursor if it represents more than
* one character (a selection range). This will only work if the
* TextView contains spannable text; otherwise it will do nothing.
*
* @return True if the cursor was actually moved, false otherwise.
*/
public boolean moveCursorToVisibleOffset() {
if (!(mText instanceof Spannable)) {
return false;
}
int start = getSelectionStart();
int end = getSelectionEnd();
if (start != end) {
return false;
}
// First: make sure the line is visible on screen:
int line = mLayout.getLineForOffset(start);
final int top = mLayout.getLineTop(line);
final int bottom = mLayout.getLineTop(line + 1);
final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
int vslack = (bottom - top) / 2;
if (vslack > vspace / 4)
vslack = vspace / 4;
final int vs = mScrollY;
if (top < (vs+vslack)) {
line = mLayout.getLineForVertical(vs+vslack+(bottom-top));
} else if (bottom > (vspace+vs-vslack)) {
line = mLayout.getLineForVertical(vspace+vs-vslack-(bottom-top));
}
// Next: make sure the character is visible on screen:
final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
final int hs = mScrollX;
final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
final int rightChar = mLayout.getOffsetForHorizontal(line, hspace+hs);
// line might contain bidirectional text
final int lowChar = leftChar < rightChar ? leftChar : rightChar;
final int highChar = leftChar > rightChar ? leftChar : rightChar;
int newStart = start;
if (newStart < lowChar) {
newStart = lowChar;
} else if (newStart > highChar) {
newStart = highChar;
}
if (newStart != start) {
Selection.setSelection((Spannable)mText, newStart);
return true;
}
return false;
}
@Override
public void computeScroll() {
if (mScroller != null) {
if (mScroller.computeScrollOffset()) {
mScrollX = mScroller.getCurrX();
mScrollY = mScroller.getCurrY();
invalidateParentCaches();
postInvalidate(); // So we draw again
}
}
}
private void getInterestingRect(Rect r, int line) {
convertFromViewportToContentCoordinates(r);
// Rectangle can can be expanded on first and last line to take
// padding into account.
// TODO Take left/right padding into account too?
if (line == 0) r.top -= getExtendedPaddingTop();
if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
}
private void convertFromViewportToContentCoordinates(Rect r) {
final int horizontalOffset = viewportToContentHorizontalOffset();
r.left += horizontalOffset;
r.right += horizontalOffset;
final int verticalOffset = viewportToContentVerticalOffset();
r.top += verticalOffset;
r.bottom += verticalOffset;
}
private int viewportToContentHorizontalOffset() {
return getCompoundPaddingLeft() - mScrollX;
}
private int viewportToContentVerticalOffset() {
int offset = getExtendedPaddingTop() - mScrollY;
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
offset += getVerticalOffset(false);
}
return offset;
}
@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}.
*/
@ViewDebug.ExportedProperty(category = "text")
public int getSelectionStart() {
return Selection.getSelectionStart(getText());
}
/**
* Convenience for {@link Selection#getSelectionEnd}.
*/
@ViewDebug.ExportedProperty(category = "text")
public int getSelectionEnd() {
return Selection.getSelectionEnd(getText());
}
/**
* Return true iff there is a selection inside this text view.
*/
public boolean hasSelection() {
final int selectionStart = getSelectionStart();
final int selectionEnd = getSelectionEnd();
return selectionStart >= 0 && selectionStart != selectionEnd;
}
/**
* 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);
}
/**
* Sets the properties of this field to transform input to ALL CAPS
* display. This may use a "small caps" formatting if available.
* This setting will be ignored if this field is editable or selectable.
*
* This call replaces the current transformation method. Disabling this
* will not necessarily restore the previous behavior from before this
* was enabled.
*
* @see #setTransformationMethod(TransformationMethod)
* @attr ref android.R.styleable#TextView_textAllCaps
*/
public void setAllCaps(boolean allCaps) {
if (allCaps) {
setTransformationMethod(new AllCapsTransformationMethod(getContext()));
} else {
setTransformationMethod(null);
}
}
/**
* If true, sets the properties of this field (number of lines, horizontally scrolling,
* transformation method) to be for a single-line input; if false, restores these to the default
* conditions.
*
* Note that the default conditions are not necessarily those that were in effect prior this
* method, and you may want to reset these properties to your custom values.
*
* @attr ref android.R.styleable#TextView_singleLine
*/
@android.view.RemotableViewMethod
public void setSingleLine(boolean singleLine) {
// Could be used, but may break backward compatibility.
// if (mSingleLine == singleLine) return;
setInputTypeSingleLine(singleLine);
applySingleLine(singleLine, true, true);
}
/**
* Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
* @param singleLine
*/
private void setInputTypeSingleLine(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;
}
}
}
private void applySingleLine(boolean singleLine, boolean applyTransformation,
boolean changeMaxLines) {
mSingleLine = singleLine;
if (singleLine) {
setLines(1);
setHorizontallyScrolling(true);
if (applyTransformation) {
setTransformationMethod(SingleLineTransformationMethod.getInstance());
}
} else {
if (changeMaxLines) {
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 to a single line. Use null
* 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) {
if (mCursorVisible != visible) {
mCursorVisible = visible;
invalidate();
makeBlink();
// InsertionPointCursorController depends on mCursorVisible
prepareCursorControllers();
}
}
private boolean isCursorVisible() {
return mCursorVisible && isTextEditable();
}
private boolean canMarquee() {
int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
return width > 0 && mLayout.getLineWidth(0) > width;
}
private void startMarquee() {
// Do not ellipsize EditText
if (mInput != null) return;
if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
return;
}
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 float MARQUEE_DELTA_MAX = 0.07f;
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 WeakReferencetext
, the lengthAfter
characters
* beginning at start
have just replaced old text that had
* length lengthBefore
. It is an error to attempt to make
* changes to text
from this callback.
*
* @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 lengthBefore The length of the former text that has been replaced
* @param lengthAfter The length of the replacement modified text
*/
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
// intentionally empty
}
/**
* 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) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
}
/**
* Adds a TextWatcher to the list of those whose methods are called
* whenever this TextView's text changes.
*