/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import android.R; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.CompatibilityInfo; 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.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.Spanned; import android.text.SpannedString; import android.text.StaticLayout; import android.text.TextDirectionHeuristic; import android.text.TextDirectionHeuristics; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; 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.CharacterStyle; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; import android.text.style.URLSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; import android.util.FloatMath; import android.util.Log; import android.util.TypedValue; import android.view.ActionMode; import android.view.DragEvent; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup.LayoutParams; import android.view.ViewRootImpl; import android.view.ViewTreeObserver; 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.view.textservice.SpellCheckerSubtype; import android.view.textservice.TextServicesManager; import android.widget.RemoteViews.RemoteView; import com.android.internal.util.FastMath; import com.android.internal.widget.EditableInputConnection; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Locale; /** * 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_drawableStart
* @attr ref android.R.styleable#TextView_drawableEnd
* @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;
// 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;
/**
* Draw marquee text with fading edges as usual
*/
private static final int MARQUEE_FADE_NORMAL = 0;
/**
* Draw marquee text as ellipsize end while inactive instead of with the fade.
* (Useful for devices where the fade can be expensive if overdone)
*/
private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
/**
* Draw marquee text with fading edges because it is currently active/animating.
*/
private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
private static final int LINES = 1;
private static final int EMS = LINES;
private static final int PIXELS = 2;
private static final RectF TEMP_RECTF = new RectF();
// XXX should be much larger
private static final int VERY_WIDE = 1024*1024;
private static final int ANIMATED_SCROLL_GAP = 250;
private static final InputFilter[] NO_FILTERS = new InputFilter[0];
private static final Spanned EMPTY_SPANNED = new SpannedString("");
private static final int CHANGE_WATCHER_PRIORITY = 100;
// New state used to change background based on whether this TextView is multiline.
private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
// System wide time for last cut or copy action.
static long LAST_CUT_OR_COPY_TIME;
private ColorStateList mTextColor;
private ColorStateList mHintTextColor;
private ColorStateList mLinkTextColor;
private int mCurTextColor;
private int mCurHintTextColor;
private boolean mFreezesText;
private boolean mTemporaryDetach;
private boolean mDispatchTemporaryDetach;
private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
private float mShadowRadius, mShadowDx, mShadowDy;
private boolean mPreDrawRegistered;
private TextUtils.TruncateAt mEllipsize;
static 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;
}
Drawables mDrawables;
private CharWrapper mCharWrapper;
private Marquee mMarquee;
private boolean mRestartMarquee;
private int mMarqueeRepeatLimit = 3;
// The alignment to pass to Layout, or null if not resolved.
private Layout.Alignment mLayoutAlignment;
private boolean mResolvedDrawables;
/**
* On some devices the fading edges add a performance penalty if used
* extensively in the same layout. This mode indicates how the marquee
* is currently being shown, if applicable. (mEllipsize will == MARQUEE)
*/
private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
/**
* When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
* the layout that should be used when the mode switches.
*/
private Layout mSavedMarqueeModeLayout;
@ViewDebug.ExportedProperty(category = "text")
private CharSequence mText;
private CharSequence mTransformed;
private BufferType mBufferType = BufferType.NORMAL;
private CharSequence mHint;
private Layout mHintLayout;
private MovementMethod mMovement;
private TransformationMethod mTransformation;
private boolean mAllowTransformationLengthChange;
private ChangeWatcher mChangeWatcher;
private ArrayList
* 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) {
createEditorIfNeeded("input is not null");
try {
mEditor.mInputType = mEditor.mKeyListener.getInputType();
} catch (IncompatibleClassChangeError e) {
mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
}
// Change inputType, without affecting transformation.
// No need to applySingleLine since mSingleLine is unchanged.
setInputTypeSingleLine(mSingleLine);
} else {
if (mEditor != null) mEditor.mInputType = EditorInfo.TYPE_NULL;
}
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
}
private void setKeyListenerOnly(KeyListener input) {
if (mEditor == null && input == null) return; // null is the default value
createEditorIfNeeded("setKeyListenerOnly");
if (mEditor.mKeyListener != input) {
mEditor.mKeyListener = input;
if (input != 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) {
if (mMovement != movement) {
mMovement = movement;
if (movement != null && !(mText instanceof Spannable)) {
setText(mText);
}
fixFocusableAndClickableSettings();
// SelectionModifierCursorController depends on textCanBeSelected, which depends on
// mMovement
if (mEditor != null) mEditor.prepareCursorControllers();
}
}
private void fixFocusableAndClickableSettings() {
if (mMovement != null || (mEditor != null && mEditor.mKeyListener != 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 = !isTextSelectable() && !(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.
*/
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.
*/
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.
*/
public int getTotalPaddingStart() {
return getCompoundPaddingStart();
}
/**
* Returns the total end padding of the view, including the end
* Drawable if any.
*/
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
*/
@android.view.RemotableViewMethod
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
*/
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
*/
@android.view.RemotableViewMethod
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
*/
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.
*/
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
*/
@android.view.RemotableViewMethod
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();
}
@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
if (start != getPaddingStart() ||
end != getPaddingEnd() ||
top != mPaddingTop ||
bottom != mPaddingBottom) {
nullLayouts();
}
// the super call will requestLayout()
super.setPaddingRelative(start, top, end, 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.
*/
@ViewDebug.ExportedProperty(category = "text")
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 the color used to paint the hint text. Return the current color selected to paint the hint text. Returns the color used to paint links in the text. For 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 Editor.InputContentType ict = mEditor == null ? null : mEditor.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_FORWARD);
if (v != null) {
if (!v.requestFocus(FOCUS_FORWARD)) {
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_BACKWARD);
if (v != null) {
if (!v.requestFocus(FOCUS_BACKWARD)) {
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;
}
}
ViewRootImpl viewRootImpl = getViewRootImpl();
if (viewRootImpl != null) {
long eventTime = SystemClock.uptimeMillis();
viewRootImpl.dispatchKeyFromIme(
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));
viewRootImpl.dispatchKeyFromIme(
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) {
createEditorIfNeeded("Private IME option set");
mEditor.createInputContentTypeIfNeeded();
mEditor.mInputContentType.privateImeOptions = type;
}
/**
* Get the private type of the content.
*
* @see #setPrivateImeOptions(String)
* @see EditorInfo#privateImeOptions
*/
public String getPrivateImeOptions() {
return mEditor != null && mEditor.mInputContentType != null
? mEditor.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 {
createEditorIfNeeded("Input extra set");
XmlResourceParser parser = getResources().getXml(xmlResId);
mEditor.createInputContentTypeIfNeeded();
mEditor.mInputContentType.extras = new Bundle();
getResources().parseBundleExtras(parser, mEditor.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 (mEditor == null && !create) return null;
createEditorIfNeeded("get Input extra");
if (mEditor.mInputContentType == null) {
if (!create) return null;
mEditor.createInputContentTypeIfNeeded();
}
if (mEditor.mInputContentType.extras == null) {
if (!create) return null;
mEditor.mInputContentType.extras = new Bundle();
}
return mEditor.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 ArrayListmult
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) {
if (mSpacingAdd != add || mSpacingMult != mult) {
mSpacingAdd = add;
mSpacingMult = mult;
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) {
// Text needs to be redrawn with the new color
if (mEditor != null) mEditor.invalidateTextDisplayList();
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);
}
}
}
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
// Save state if we are forced to
boolean save = mFreezesText;
int start = 0;
int end = 0;
if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
save = true;
}
}
if (save) {
SavedState ss = new SavedState(superState);
// XXX Should also save the current scroll position!
ss.selStart = start;
ss.selEnd = end;
if (mText instanceof Spanned) {
/*
* Calling setText() strips off any ChangeWatchers;
* strip them now to avoid leaking references.
* But do it to a copy so that if there are any
* further changes to the text of this view, it
* won't get into an inconsistent state.
*/
Spannable sp = new SpannableString(mText);
for (ChangeWatcher cw : sp.getSpans(0, sp.length(), ChangeWatcher.class)) {
sp.removeSpan(cw);
}
if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}
ss.text = sp;
} else {
ss.text = mText.toString();
}
if (isFocused() && start >= 0 && end >= 0) {
ss.frozenWithFocus = true;
}
ss.error = getError();
return ss;
}
return superState;
}
void removeMisspelledSpans(Spannable spannable) {
SuggestionSpan[] suggestionSpans = spannable.getSpans(0, spannable.length(),
SuggestionSpan.class);
for (int i = 0; i < suggestionSpans.length; i++) {
int flags = suggestionSpans[i].getFlags();
if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
&& (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
spannable.removeSpan(suggestionSpans[i]);
}
}
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
SavedState ss = (SavedState)state;
super.onRestoreInstanceState(ss.getSuperState());
// XXX restore buffer type too, as well as lots of other stuff
if (ss.text != null) {
setText(ss.text);
}
if (ss.selStart >= 0 && ss.selEnd >= 0) {
if (mText instanceof Spannable) {
int len = mText.length();
if (ss.selStart > len || ss.selEnd > len) {
String restored = "";
if (ss.text != null) {
restored = "(restored) ";
}
Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
"/" + ss.selEnd + " out of range for " + restored +
"text " + mText);
} else {
Selection.setSelection((Spannable) mText, ss.selStart, ss.selEnd);
if (ss.frozenWithFocus) {
createEditorIfNeeded("restore instance with focus");
mEditor.mFrozenWithFocus = true;
}
}
}
}
if (ss.error != null) {
final CharSequence error = ss.error;
// Display the error later, after the first layout pass
post(new Runnable() {
public void run() {
setError(error);
}
});
}
}
/**
* Control whether this text view saves its entire text contents when
* freezing to an icicle, in addition to dynamic state such as cursor
* position. By default this is false, not saving the text. Set to true
* if the text in the text view is not being saved somewhere else in
* persistent storage (such as in a content provider) so that if the
* view is later thawed the user will not lose their data.
*
* @param freezesText Controls whether a frozen icicle should include the
* entire text data: true to include it, false to not.
*
* @attr ref android.R.styleable#TextView_freezesText
*/
@android.view.RemotableViewMethod
public void setFreezesText(boolean freezesText) {
mFreezesText = freezesText;
}
/**
* Return whether this text view is including its entire text contents
* in frozen icicles.
*
* @return Returns true if text is included, false if it isn't.
*
* @see #setFreezesText
*/
public boolean getFreezesText() {
return mFreezesText;
}
///////////////////////////////////////////////////////////////////////////
/**
* Sets the Factory used to create new Editables.
*/
public final void setEditableFactory(Editable.Factory factory) {
mEditableFactory = factory;
setText(mText);
}
/**
* Sets the Factory used to create new Spannables.
*/
public final void setSpannableFactory(Spannable.Factory factory) {
mSpannableFactory = factory;
setText(mText);
}
/**
* Sets the string value of the TextView. TextView does not accept
* HTML-like formatting, which you can do with text strings in XML resource files.
* To style your strings, attach android.text.style.* objects to a
* {@link android.text.SpannableString SpannableString}, or see the
*
* Available Resource Types documentation for an example of setting
* formatted text in the XML resource file.
*
* @attr ref android.R.styleable#TextView_text
*/
@android.view.RemotableViewMethod
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
/**
* Like {@link #setText(CharSequence)},
* except that the cursor position (if any) is retained in the new text.
*
* @param text The new text to place in the text view.
*
* @see #setText(CharSequence)
*/
@android.view.RemotableViewMethod
public final void setTextKeepState(CharSequence text) {
setTextKeepState(text, mBufferType);
}
/**
* Sets the text that this TextView is to display (see
* {@link #setText(CharSequence)}) and also sets whether it is stored
* in a styleable/spannable buffer and whether it is editable.
*
* @attr ref android.R.styleable#TextView_text
* @attr ref android.R.styleable#TextView_bufferType
*/
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
if (mCharWrapper != null) {
mCharWrapper.mChars = null;
}
}
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
if (text == null) {
text = "";
}
// If suggestions are not enabled, remove the suggestion spans from the text
if (!isSuggestionsEnabled()) {
text = removeSuggestionSpans(text);
}
if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
if (text instanceof Spanned &&
((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
setHorizontalFadingEdgeEnabled(true);
mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
} else {
setHorizontalFadingEdgeEnabled(false);
mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
}
setEllipsize(TextUtils.TruncateAt.MARQUEE);
}
int n = mFilters.length;
for (int i = 0; i < n; i++) {
CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
if (out != null) {
text = out;
}
}
if (notifyBefore) {
if (mText != null) {
oldlen = mText.length();
sendBeforeTextChanged(mText, 0, oldlen, text.length());
} else {
sendBeforeTextChanged("", 0, 0, text.length());
}
}
boolean needEditableForNotification = false;
if (mListeners != null && mListeners.size() != 0) {
needEditableForNotification = true;
}
if (type == BufferType.EDITABLE || getKeyListener() != null ||
needEditableForNotification) {
createEditorIfNeeded("setText with BufferType.EDITABLE or non null mInput");
Editable t = mEditableFactory.newEditable(text);
text = t;
setFilters(t, mFilters);
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
} else if (type == BufferType.SPANNABLE || mMovement != null) {
text = mSpannableFactory.newSpannable(text);
} else if (!(text instanceof CharWrapper)) {
text = TextUtils.stringOrSpannedString(text);
}
if (mAutoLinkMask != 0) {
Spannable s2;
if (type == BufferType.EDITABLE || text instanceof Spannable) {
s2 = (Spannable) text;
} else {
s2 = mSpannableFactory.newSpannable(text);
}
if (Linkify.addLinks(s2, mAutoLinkMask)) {
text = s2;
type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
/*
* We must go ahead and set the text before changing the
* movement method, because setMovementMethod() may call
* setText() again to try to upgrade the buffer type.
*/
mText = text;
// Do not change the movement method for text that support text selection as it
// would prevent an arbitrary cursor displacement.
if (mLinksClickable && !textCanBeSelected()) {
setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
mBufferType = type;
mText = text;
if (mTransformation == null) {
mTransformed = text;
} else {
mTransformed = mTransformation.getTransformation(text, this);
}
final int textLength = text.length();
if (text instanceof Spannable && !mAllowTransformationLengthChange) {
Spannable sp = (Spannable) text;
// Remove any ChangeWatchers that might have come from other TextViews.
final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
final int count = watchers.length;
for (int i = 0; i < count; i++) {
sp.removeSpan(watchers[i]);
}
if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();
sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
(CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
if (mEditor != null) mEditor.addSpanWatchers(sp);
if (mTransformation != null) {
sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
if (mMovement != null) {
mMovement.initialize(this, (Spannable) text);
/*
* Initializing the movement method will have set the
* selection, so reset mSelectionMoved to keep that from
* interfering with the normal on-focus selection-setting.
*/
if (mEditor != null) mEditor.mSelectionMoved = false;
}
}
if (mLayout != null) {
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
if (mEditor != null) mEditor.prepareCursorControllers();
}
/**
* Sets the TextView to display the specified slice of the specified
* char array. You must promise that you will not change the contents
* of the array except for right before another call to setText(),
* since the TextView has no way to know that the text
* has changed and that it needs to invalidate and re-layout.
*/
public final void setText(char[] text, int start, int len) {
int oldlen = 0;
if (start < 0 || len < 0 || start + len > text.length) {
throw new IndexOutOfBoundsException(start + ", " + len);
}
/*
* We must do the before-notification here ourselves because if
* the old text is a CharWrapper we destroy it before calling
* into the normal path.
*/
if (mText != null) {
oldlen = mText.length();
sendBeforeTextChanged(mText, 0, oldlen, len);
} else {
sendBeforeTextChanged("", 0, 0, len);
}
if (mCharWrapper == null) {
mCharWrapper = new CharWrapper(text, start, len);
} else {
mCharWrapper.set(text, start, len);
}
setText(mCharWrapper, mBufferType, false, oldlen);
}
/**
* Like {@link #setText(CharSequence, android.widget.TextView.BufferType)},
* except that the cursor position (if any) is retained in the new text.
*
* @see #setText(CharSequence, android.widget.TextView.BufferType)
*/
public final void setTextKeepState(CharSequence text, BufferType type) {
int start = getSelectionStart();
int end = getSelectionEnd();
int len = text.length();
setText(text, type);
if (start >= 0 || end >= 0) {
if (mText instanceof Spannable) {
Selection.setSelection((Spannable) mText,
Math.max(0, Math.min(start, len)),
Math.max(0, Math.min(end, len)));
}
}
}
@android.view.RemotableViewMethod
public final void setText(int resid) {
setText(getContext().getResources().getText(resid));
}
public final void setText(int resid, BufferType type) {
setText(getContext().getResources().getText(resid), type);
}
/**
* Sets the text to be displayed when the text of the TextView is empty.
* Null means to use the normal empty text. The hint does not currently
* participate in determining the size of the view.
*
* @attr ref android.R.styleable#TextView_hint
*/
@android.view.RemotableViewMethod
public final void setHint(CharSequence hint) {
mHint = TextUtils.stringOrSpannedString(hint);
if (mLayout != null) {
checkForRelayout();
}
if (mText.length() == 0) {
invalidate();
}
// Invalidate display list if hint is currently used
if (mEditor != null && mText.length() == 0 && mHint != null) {
mEditor.invalidateTextDisplayList();
}
}
/**
* Sets the text to be displayed when the text of the TextView is empty,
* from a resource.
*
* @attr ref android.R.styleable#TextView_hint
*/
@android.view.RemotableViewMethod
public final void setHint(int resid) {
setHint(getContext().getResources().getText(resid));
}
/**
* Returns the hint that is displayed when the text of the TextView
* is empty.
*
* @attr ref android.R.styleable#TextView_hint
*/
@ViewDebug.CapturedViewProperty
public CharSequence getHint() {
return mHint;
}
boolean isSingleLine() {
return mSingleLine;
}
private static boolean isMultilineInputType(int type) {
return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE)) ==
(EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
}
/**
* Removes the suggestion spans.
*/
CharSequence removeSuggestionSpans(CharSequence text) {
if (text instanceof Spanned) {
Spannable spannable;
if (text instanceof Spannable) {
spannable = (Spannable) text;
} else {
spannable = new SpannableString(text);
text = spannable;
}
SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
for (int i = 0; i < spans.length; i++) {
spannable.removeSpan(spans[i]);
}
}
return text;
}
/**
* Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
* will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
* to match the given content type. If the given content type is {@link EditorInfo#TYPE_NULL}
* then a soft keyboard will not be displayed for this text view.
*
* Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
* modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
* type.
*
* @see #getInputType()
* @see #setRawInputType(int)
* @see android.text.InputType
* @attr ref android.R.styleable#TextView_inputType
*/
public void setInputType(int type) {
final boolean wasPassword = isPasswordInputType(getInputType());
final boolean wasVisiblePassword = isVisiblePasswordInputType(getInputType());
setInputType(type, false);
final boolean isPassword = isPasswordInputType(type);
final boolean isVisiblePassword = isVisiblePasswordInputType(type);
boolean forceUpdate = false;
if (isPassword) {
setTransformationMethod(PasswordTransformationMethod.getInstance());
setTypefaceByIndex(MONOSPACE, 0);
} else if (isVisiblePassword) {
if (mTransformation == PasswordTransformationMethod.getInstance()) {
forceUpdate = true;
}
setTypefaceByIndex(MONOSPACE, 0);
} else if (wasPassword || wasVisiblePassword) {
// not in password mode, clean up typeface and transformation
setTypefaceByIndex(-1, -1);
if (mTransformation == PasswordTransformationMethod.getInstance()) {
forceUpdate = true;
}
}
boolean singleLine = !isMultilineInputType(type);
// We need to update the single line mode if it has changed or we
// were previously in password mode.
if (mSingleLine != singleLine || forceUpdate) {
// Change single line mode, but only change the transformation if
// we are not in password mode.
applySingleLine(singleLine, !isPassword, true);
}
if (!isSuggestionsEnabled()) {
mText = removeSuggestionSpans(mText);
}
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
}
/**
* It would be better to rely on the input type for everything. A password inputType should have
* a password transformation. We should hence use isPasswordInputType instead of this method.
*
* We should:
* - Call setInputType in setKeyListener instead of changing the input type directly (which
* would install the correct transformation).
* - Refuse the installation of a non-password transformation in setTransformation if the input
* type is password.
*
* However, this is like this for legacy reasons and we cannot break existing apps. This method
* is useful since it matches what the user can see (obfuscated text or not).
*
* @return true if the current transformation method is of the password type.
*/
private boolean hasPasswordTransformationMethod() {
return mTransformation instanceof PasswordTransformationMethod;
}
private static boolean isPasswordInputType(int inputType) {
final int variation =
inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
return variation
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
|| variation
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
|| variation
== (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
}
private static boolean isVisiblePasswordInputType(int inputType) {
final int variation =
inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
return variation
== (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
}
/**
* Directly change the content type integer of the text view, without
* modifying any other state.
* @see #setInputType(int)
* @see android.text.InputType
* @attr ref android.R.styleable#TextView_inputType
*/
public void setRawInputType(int type) {
if (type == InputType.TYPE_NULL && mEditor == null) return; //TYPE_NULL is the default value
createEditorIfNeeded("non null input type");
mEditor.mInputType = type;
}
private void setInputType(int type, boolean direct) {
final int cls = type & EditorInfo.TYPE_MASK_CLASS;
KeyListener input;
if (cls == EditorInfo.TYPE_CLASS_TEXT) {
boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
TextKeyListener.Capitalize cap;
if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
cap = TextKeyListener.Capitalize.CHARACTERS;
} else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
cap = TextKeyListener.Capitalize.WORDS;
} else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
cap = TextKeyListener.Capitalize.SENTENCES;
} else {
cap = TextKeyListener.Capitalize.NONE;
}
input = TextKeyListener.getInstance(autotext, cap);
} else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
input = DigitsKeyListener.getInstance(
(type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
(type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
} else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
switch (type & EditorInfo.TYPE_MASK_VARIATION) {
case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
input = DateKeyListener.getInstance();
break;
case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
input = TimeKeyListener.getInstance();
break;
default:
input = DateTimeKeyListener.getInstance();
break;
}
} else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
input = DialerKeyListener.getInstance();
} else {
input = TextKeyListener.getInstance();
}
setRawInputType(type);
if (direct) {
createEditorIfNeeded("setInputType");
mEditor.mKeyListener = input;
} else {
setKeyListenerOnly(input);
}
}
/**
* Get the type of the editable content.
*
* @see #setInputType(int)
* @see android.text.InputType
*/
public int getInputType() {
return mEditor == null ? EditorInfo.TYPE_NULL : mEditor.mInputType;
}
/**
* Change the editor type integer associated with the text view, which
* will be reported to an IME with {@link EditorInfo#imeOptions} when it
* has focus.
* @see #getImeOptions
* @see android.view.inputmethod.EditorInfo
* @attr ref android.R.styleable#TextView_imeOptions
*/
public void setImeOptions(int imeOptions) {
createEditorIfNeeded("IME options specified");
mEditor.createInputContentTypeIfNeeded();
mEditor.mInputContentType.imeOptions = imeOptions;
}
/**
* Get the type of the IME editor.
*
* @see #setImeOptions(int)
* @see android.view.inputmethod.EditorInfo
*/
public int getImeOptions() {
return mEditor != null && mEditor.mInputContentType != null
? mEditor.mInputContentType.imeOptions : EditorInfo.IME_NULL;
}
/**
* Change the custom IME action associated with the text view, which
* will be reported to an IME with {@link EditorInfo#actionLabel}
* and {@link EditorInfo#actionId} when it has focus.
* @see #getImeActionLabel
* @see #getImeActionId
* @see android.view.inputmethod.EditorInfo
* @attr ref android.R.styleable#TextView_imeActionLabel
* @attr ref android.R.styleable#TextView_imeActionId
*/
public void setImeActionLabel(CharSequence label, int actionId) {
createEditorIfNeeded("IME action label specified");
mEditor.createInputContentTypeIfNeeded();
mEditor.mInputContentType.imeActionLabel = label;
mEditor.mInputContentType.imeActionId = actionId;
}
/**
* Get the IME action label previous set with {@link #setImeActionLabel}.
*
* @see #setImeActionLabel
* @see android.view.inputmethod.EditorInfo
*/
public CharSequence getImeActionLabel() {
return mEditor != null && mEditor.mInputContentType != null
? mEditor.mInputContentType.imeActionLabel : null;
}
/**
* Get the IME action ID previous set with {@link #setImeActionLabel}.
*
* @see #setImeActionLabel
* @see android.view.inputmethod.EditorInfo
*/
public int getImeActionId() {
return mEditor != null && mEditor.mInputContentType != null
? mEditor.mInputContentType.imeActionId : 0;
}
/**
* Set a special listener to be called when an action is performed
* on the text view. This will be called when the enter key is pressed,
* or when an action supplied to the IME is selected by the user. Setting
* this means that the normal hard key event will not insert a newline
* into the text view, even if it is multi-line; holding down the ALT
* modifier will, however, allow the user to insert a newline character.
*/
public void setOnEditorActionListener(OnEditorActionListener l) {
createEditorIfNeeded("Editor action listener set");
mEditor.createInputContentTypeIfNeeded();
mEditor.mInputContentType.onEditorActionListener = l;
}
/**
* Called when an attached input method calls
* {@link InputConnection#performEditorAction(int)
* InputConnection.performEditorAction()}
* for this text view. The default implementation will call your action
* listener supplied to {@link #setOnEditorActionListener}, or perform
* a standard operation for {@link EditorInfo#IME_ACTION_NEXT
* EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
* EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
* EditorInfo.IME_ACTION_DONE}.
*
* null
if no error was set
* or if it the error was cleared by the widget after user input.
*/
public CharSequence getError() {
return mEditor == null ? null : mEditor.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) {
createEditorIfNeeded("setError");
mEditor.setError(error, icon);
}
@Override
protected boolean setFrame(int l, int t, int r, int b) {
boolean result = super.setFrame(l, t, r, b);
if (mEditor != null) mEditor.setFrame();
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 (mEditor != null && mEditor.mKeyListener instanceof InputFilter) {
InputFilter[] nf = new InputFilter[filters.length + 1];
System.arraycopy(filters, 0, nf, 0, filters.length);
nf[filters.length] = (InputFilter) mEditor.mKeyListener;
e.setFilters(nf);
} else {
e.setFilters(filters);
}
}
/**
* Returns the current list of input filters.
*/
public InputFilter[] getFilters() {
return mFilters;
}
/////////////////////////////////////////////////////////////////////////
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;
}
void invalidateCursorPath() {
if (mHighlightPathBogus) {
invalidateCursor();
} else {
final int horizontalPadding = getCompoundPaddingLeft();
final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
if (mEditor.mCursorCount == 0) {
synchronized (TEMP_RECTF) {
/*
* 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 is guaranteed to be non null at that point.
mHighlightPath.computeBounds(TEMP_RECTF, false);
invalidate((int) FloatMath.floor(horizontalPadding + TEMP_RECTF.left - thick),
(int) FloatMath.floor(verticalPadding + TEMP_RECTF.top - thick),
(int) FloatMath.ceil(horizontalPadding + TEMP_RECTF.right + thick),
(int) FloatMath.ceil(verticalPadding + TEMP_RECTF.bottom + thick));
}
} else {
for (int i = 0; i < mEditor.mCursorCount; i++) {
Rect bounds = mEditor.mCursorDrawable[i].getBounds();
invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
}
}
}
}
void invalidateCursor() {
int where = getSelectionEnd();
invalidateCursor(where, where, where);
}
private void invalidateCursor(int a, int b, int c) {
if (a >= 0 || b >= 0 || c >= 0) {
int start = Math.min(Math.min(a, b), c);
int end = Math.max(Math.max(a, b), c);
invalidateRegion(start, end, true /* Also invalidates blinking cursor */);
}
}
/**
* Invalidates the region of text enclosed between the start and end text offsets.
*/
void invalidateRegion(int start, int end, boolean invalidateCursor) {
if (mLayout == null) {
invalidate();
} else {
int lineStart = mLayout.getLineForOffset(start);
int top = mLayout.getLineTop(lineStart);
// 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 (lineStart > 0) {
top -= mLayout.getLineDescent(lineStart - 1);
}
int lineEnd;
if (start == end)
lineEnd = lineStart;
else
lineEnd = mLayout.getLineForOffset(end);
int bottom = mLayout.getLineBottom(lineEnd);
// mEditor can be null in case selection is set programmatically.
if (invalidateCursor && mEditor != null) {
for (int i = 0; i < mEditor.mCursorCount; i++) {
Rect bounds = mEditor.mCursorDrawable[i].getBounds();
top = Math.min(top, bounds.top);
bottom = Math.max(bottom, bounds.bottom);
}
}
final int compoundPaddingLeft = getCompoundPaddingLeft();
final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
int left, right;
if (lineStart == lineEnd && !invalidateCursor) {
left = (int) mLayout.getPrimaryHorizontal(start);
right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0);
left += compoundPaddingLeft;
right += compoundPaddingLeft;
} else {
// Rectangle bounding box when the region spans several lines
left = compoundPaddingLeft;
right = getWidth() - getCompoundPaddingRight();
}
invalidate(mScrollX + left, verticalPadding + top,
mScrollX + right, verticalPadding + bottom);
}
}
private void registerForPreDraw() {
if (!mPreDrawRegistered) {
getViewTreeObserver().addOnPreDrawListener(this);
mPreDrawRegistered = true;
}
}
/**
* {@inheritDoc}
*/
public boolean onPreDraw() {
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 (mEditor != null && mEditor.mSelectionModifierCursorController != null &&
mEditor.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 (mEditor != null && mEditor.mCreatedWithASelection) {
mEditor.startSelectionActionMode();
mEditor.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() && mEditor != null) {
mEditor.startSelectionActionMode();
}
getViewTreeObserver().removeOnPreDrawListener(this);
mPreDrawRegistered = false;
return !changed;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mTemporaryDetach = false;
// Resolve drawables as the layout direction has been resolved
resolveDrawables();
if (mEditor != null) mEditor.onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mPreDrawRegistered) {
getViewTreeObserver().removeOnPreDrawListener(this);
mPreDrawRegistered = false;
}
resetResolvedDrawables();
if (mEditor != null) mEditor.onDetachedFromWindow();
}
@Override
public void onScreenStateChanged(int screenState) {
super.onScreenStateChanged(screenState);
if (mEditor != null) mEditor.onScreenStateChanged(screenState);
}
@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);
}
}
@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
public boolean hasOverlappingRendering() {
return (getBackground() != null || mText instanceof Spannable || hasSelection());
}
/**
* 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 mEditor == null ? false : mEditor.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 (!selectable && mEditor == null) return; // false is default value with no edit data
createEditorIfNeeded("setTextIsSelectable");
if (mEditor.mTextIsSelectable == selectable) return;
mEditor.mTextIsSelectable = selectable;
setFocusableInTouchMode(selectable);
setFocusable(selectable);
setClickable(selectable);
setLongClickable(selectable);
// mInputType should already be EditorInfo.TYPE_NULL and mInput should be 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
mEditor.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 (isTextSelectable()) {
// 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;
}
private Path getUpdatedHighlightPath() {
Path highlight = null;
Paint highlightPaint = mHighlightPaint;
final int selStart = getSelectionStart();
final int selEnd = getSelectionEnd();
if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) {
if (selStart == selEnd) {
if (mEditor != null && mEditor.isCursorVisible() &&
(SystemClock.uptimeMillis() - mEditor.mShowCursor) %
(2 * Editor.BLINK) < Editor.BLINK) {
if (mHighlightPathBogus) {
if (mHighlightPath == null) mHighlightPath = new Path();
mHighlightPath.reset();
mLayout.getCursorPath(selStart, mHighlightPath, mText);
mEditor.updateCursorsPositions();
mHighlightPathBogus = false;
}
// XXX should pass to skin instead of drawing directly
highlightPaint.setColor(mCurTextColor);
highlightPaint.setStyle(Paint.Style.STROKE);
highlight = mHighlightPath;
}
} else {
if (mHighlightPathBogus) {
if (mHighlightPath == null) mHighlightPath = new Path();
mHighlightPath.reset();
mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
mHighlightPathBogus = false;
}
// XXX should pass to skin instead of drawing directly
highlightPaint.setColor(mHighlightColor);
highlightPaint.setStyle(Paint.Style.FILL);
highlight = mHighlightPath;
}
}
return highlight;
}
@Override
protected void onDraw(Canvas canvas) {
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;
if (mHint != null && mText.length() == 0) {
if (mHintTextColor != null) {
color = mCurHintTextColor;
}
layout = mHintLayout;
}
mTextPaint.setColor(color);
mTextPaint.drawableState = getDrawableState();
canvas.save();
/* Would be faster if we didn't have to do this. Can we chop the
(displayable) text so that we don't need to do this ever?
*/
int extendedPaddingTop = getExtendedPaddingTop();
int extendedPaddingBottom = getExtendedPaddingBottom();
final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
final int maxScrollY = mLayout.getHeight() - vspace;
float clipLeft = compoundPaddingLeft + scrollX;
float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
float clipRight = right - left - compoundPaddingRight + scrollX;
float clipBottom = bottom - top + scrollY -
((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
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 &&
mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
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);
}
}
final int cursorOffsetVertical = voffsetCursor - voffsetText;
Path highlight = getUpdatedHighlightPath();
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
canvas.restore();
}
@Override
public void getFocusedRect(Rect r) {
if (mLayout == null) {
super.getFocusedRect(r);
return;
}
int selEnd = getSelectionEnd();
if (selEnd < 0) {
super.getFocusedRect(r);
return;
}
int selStart = getSelectionStart();
if (selStart < 0 || selStart >= selEnd) {
int line = mLayout.getLineForOffset(selEnd);
r.top = mLayout.getLineTop(line);
r.bottom = mLayout.getLineBottom(line);
r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
r.right = r.left + 4;
} else {
int lineStart = mLayout.getLineForOffset(selStart);
int lineEnd = mLayout.getLineForOffset(selEnd);
r.top = mLayout.getLineTop(lineStart);
r.bottom = mLayout.getLineBottom(lineEnd);
if (lineStart == lineEnd) {
r.left = (int) mLayout.getPrimaryHorizontal(selStart);
r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
} else {
// Selection extends across multiple lines -- make the focused
// rect cover the entire width.
if (mHighlightPathBogus) {
if (mHighlightPath == null) mHighlightPath = new Path();
mHighlightPath.reset();
mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
mHighlightPathBogus = false;
}
synchronized (TEMP_RECTF) {
mHighlightPath.computeBounds(TEMP_RECTF, true);
r.left = (int)TEMP_RECTF.left-1;
r.right = (int)TEMP_RECTF.right+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);
int paddingBottom = getExtendedPaddingBottom();
r.bottom += paddingBottom;
}
/**
* 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);
}
/**
* @hide
*/
@Override
protected int getFadeTop(boolean offsetRequired) {
if (mLayout == null) return 0;
int voffset = 0;
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
voffset = getVerticalOffset(true);
}
if (offsetRequired) voffset += getTopPaddingOffset();
return getExtendedPaddingTop() + voffset;
}
/**
* @hide
*/
@Override
protected int getFadeHeight(boolean offsetRequired) {
return mLayout != null ? mLayout.getHeight() : 0;
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
boolean isInSelectionMode = mEditor != null && mEditor.mSelectionActionMode != null;
if (isInSelectionMode) {
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.handleUpEvent(event);
}
if (event.isTracking() && !event.isCanceled()) {
stopSelectionActionMode();
return true;
}
}
}
}
return super.onKeyPreIme(keyCode, event);
}
@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) {
// mEditor and mEditor.mInput are not null from doKeyDown
mEditor.mKeyListener.onKeyUp(this, (Editable)mText, keyCode, up);
while (--repeatCount > 0) {
mEditor.mKeyListener.onKeyDown(this, (Editable)mText, keyCode, down);
mEditor.mKeyListener.onKeyUp(this, (Editable)mText, keyCode, up);
}
hideErrorIfUnchanged();
} else if (which == 2) {
// mMovement is not null from doKeyDown
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 (getKeyListener() == null) {
return false;
}
if (mSingleLine) {
return true;
}
if (mEditor != null &&
(mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
int variation = mEditor.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 (getKeyListener() != null && !mSingleLine && mEditor != null &&
(mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
int variation = mEditor.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:
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 (mEditor != null && mEditor.mInputContentType != null) {
// If there is an action listener, given them a
// chance to consume the event.
if (mEditor.mInputContentType.onEditorActionListener != null &&
mEditor.mInputContentType.onEditorActionListener.onEditorAction(
this, EditorInfo.IME_NULL, event)) {
mEditor.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 (hasOnClickListeners()) {
return 0;
}
return -1;
}
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
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 (mEditor != null && mEditor.mSelectionActionMode != null) {
stopSelectionActionMode();
return -1;
}
break;
}
if (mEditor != null && mEditor.mKeyListener != null) {
resetErrorChangedFlag();
boolean doDown = true;
if (otherEvent != null) {
try {
beginBatchEdit();
final boolean handled = mEditor.mKeyListener.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 = mEditor.mKeyListener.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.
*/
if (mEditor != null) mEditor.mErrorWasChanged = false;
}
/**
* @hide
*/
public void hideErrorIfUnchanged() {
if (mEditor != null && mEditor.mError != null && !mEditor.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:
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 (!hasOnClickListeners()) {
if (mMovement != null && mText instanceof Editable
&& mLayout != null && onCheckIsTextEditor()) {
InputMethodManager imm = InputMethodManager.peekInstance();
viewClicked(imm);
if (imm != null && getShowSoftInputOnFocus()) {
imm.showSoftInput(this, 0);
}
}
}
}
return super.onKeyUp(keyCode, event);
case KeyEvent.KEYCODE_ENTER:
if (event.hasNoModifiers()) {
if (mEditor != null && mEditor.mInputContentType != null
&& mEditor.mInputContentType.onEditorActionListener != null
&& mEditor.mInputContentType.enterDown) {
mEditor.mInputContentType.enterDown = false;
if (mEditor.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 (!hasOnClickListeners()) {
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 (mEditor != null && mEditor.mKeyListener != null)
if (mEditor.mKeyListener.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 mEditor != null && mEditor.mInputType != EditorInfo.TYPE_NULL;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (onCheckIsTextEditor() && isEnabled()) {
mEditor.createInputMethodStateIfNeeded();
outAttrs.inputType = getInputType();
if (mEditor.mInputContentType != null) {
outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
outAttrs.actionId = mEditor.mInputContentType.imeActionId;
outAttrs.extras = mEditor.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(getInputType());
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) {
createEditorIfNeeded("extractText");
return mEditor.extractText(request, outText);
}
/**
* 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 (mEditor.mInputMethodState != null) {
mEditor.mInputMethodState.mExtractedTextRequest = req;
}
// This would stop a possible selection mode, but no such mode is started in case
// extracted mode will start. Some text is selected though, and will trigger an action mode
// in the extracted view.
mEditor.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 (mEditor != null) mEditor.onCommitCorrection(info);
}
public void beginBatchEdit() {
if (mEditor != null) mEditor.beginBatchEdit();
}
public void endBatchEdit() {
if (mEditor != null) mEditor.endBatchEdit();
}
/**
* 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;
}
mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
mBoring = mHintBoring = null;
// Since it depends on the value of mLayout
if (mEditor != null) mEditor.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
public void onResolvedLayoutDirectionReset() {
if (mLayoutAlignment != null) {
int resolvedTextAlignment = getResolvedTextAlignment();
if (resolvedTextAlignment == TEXT_ALIGNMENT_VIEW_START ||
resolvedTextAlignment == TEXT_ALIGNMENT_VIEW_END) {
mLayoutAlignment = null;
}
}
}
private Layout.Alignment getLayoutAlignment() {
if (mLayoutAlignment == null) {
int textAlign = getResolvedTextAlignment();
switch (textAlign) {
case TEXT_ALIGNMENT_GRAVITY:
switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
case Gravity.START:
mLayoutAlignment = Layout.Alignment.ALIGN_NORMAL;
break;
case Gravity.END:
mLayoutAlignment = Layout.Alignment.ALIGN_OPPOSITE;
break;
case Gravity.LEFT:
mLayoutAlignment = Layout.Alignment.ALIGN_LEFT;
break;
case Gravity.RIGHT:
mLayoutAlignment = Layout.Alignment.ALIGN_RIGHT;
break;
case Gravity.CENTER_HORIZONTAL:
mLayoutAlignment = Layout.Alignment.ALIGN_CENTER;
break;
default:
mLayoutAlignment = Layout.Alignment.ALIGN_NORMAL;
break;
}
break;
case TEXT_ALIGNMENT_TEXT_START:
mLayoutAlignment = Layout.Alignment.ALIGN_NORMAL;
break;
case TEXT_ALIGNMENT_TEXT_END:
mLayoutAlignment = Layout.Alignment.ALIGN_OPPOSITE;
break;
case TEXT_ALIGNMENT_CENTER:
mLayoutAlignment = Layout.Alignment.ALIGN_CENTER;
break;
case TEXT_ALIGNMENT_VIEW_START:
mLayoutAlignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
break;
case TEXT_ALIGNMENT_VIEW_END:
mLayoutAlignment = (getResolvedLayoutDirection() == LAYOUT_DIRECTION_RTL) ?
Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
break;
case TEXT_ALIGNMENT_INHERIT:
// This should never happen as we have already resolved the text alignment
// but better safe than sorry so we just fall through
default:
mLayoutAlignment = Layout.Alignment.ALIGN_NORMAL;
break;
}
}
return mLayoutAlignment;
}
/**
* The width passed in is now the desired layout width,
* not the full view width with padding.
* {@hide}
*/
protected void makeNewLayout(int wantWidth, int hintWidth,
BoringLayout.Metrics boring,
BoringLayout.Metrics hintBoring,
int ellipsisWidth, boolean bringIntoView) {
stopMarquee();
// Update "old" cached values
mOldMaximum = mMaximum;
mOldMaxMode = mMaxMode;
mHighlightPathBogus = true;
if (wantWidth < 0) {
wantWidth = 0;
}
if (hintWidth < 0) {
hintWidth = 0;
}
Layout.Alignment alignment = getLayoutAlignment();
boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE &&
mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
TruncateAt effectiveEllipsize = mEllipsize;
if (mEllipsize == TruncateAt.MARQUEE &&
mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
effectiveEllipsize = TruncateAt.END_SMALL;
}
if (mTextDir == null) {
resolveTextDirection();
}
mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
effectiveEllipsize, effectiveEllipsize == mEllipsize);
if (switchEllipsize) {
TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE ?
TruncateAt.END : TruncateAt.MARQUEE;
mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
}
shouldEllipsize = mEllipsize != null;
mHintLayout = null;
if (mHint != null) {
if (shouldEllipsize) hintWidth = wantWidth;
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, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
} 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, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
} 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
if (mEditor != null) mEditor.prepareCursorControllers();
}
private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
if (mText instanceof Spannable) {
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, getKeyListener() == null ? effectiveEllipsize : 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 <= wantWidth &&
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
}
if (useSaved) {
mSavedLayout = (BoringLayout) result;
}
} else if (shouldEllipsize && boring.width <= wantWidth) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
}
} else if (shouldEllipsize) {
result = new StaticLayout(mTransformed,
0, mTransformed.length(),
mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, effectiveEllipsize,
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
} else {
result = new StaticLayout(mTransformed, mTextPaint,
wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
} else if (shouldEllipsize) {
result = new StaticLayout(mTransformed,
0, mTransformed.length(),
mTextPaint, wantWidth, alignment, mTextDir, mSpacingMult,
mSpacingAdd, mIncludePad, effectiveEllipsize,
ellipsisWidth, mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
} else {
result = new StaticLayout(mTransformed, mTextPaint,
wantWidth, alignment, mTextDir, mSpacingMult, mSpacingAdd,
mIncludePad);
}
}
return result;
}
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) {
if (mIncludePad != 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, mTextDir, 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 {
final boolean layoutChanged = (mLayout.getWidth() != want) ||
(hintWidth != hintWant) ||
(mLayout.getEllipsizedWidth() !=
width - getCompoundPaddingLeft() - getCompoundPaddingRight());
final boolean widthChanged = (mHint == null) &&
(mEllipsize == null) &&
(want > mLayout.getWidth()) &&
(mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));
final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
if (layoutChanged || maximumChanged) {
if (!maximumChanged && widthChanged) {
mLayout.increaseWidthTo(want);
} else {
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
}
} else {
// Nothing has 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;
if (mMaxMode == LINES) {
/*
* Don't cap the hint to a certain number of lines.
* (Do cap it, though, if we have a maximum pixel height.)
*/
if (cap) {
if (linecount > mMaximum) {
desired = layout.getLineTop(mMaximum);
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();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (changed && mEditor != null) mEditor.invalidateTextDisplayList();
}
private boolean isShowingHint() {
return TextUtils.isEmpty(mText) && !TextUtils.isEmpty(mHint);
}
/**
* Returns true if anything changed.
*/
private boolean bringTextIntoView() {
Layout layout = isShowingHint() ? mHintLayout : mLayout;
int line = 0;
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
line = layout.getLineCount() - 1;
}
Layout.Alignment a = layout.getParagraphAlignment(line);
int dir = layout.getParagraphDirection(line);
int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
int ht = layout.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(layout.getLineLeft(line));
int right = (int) FloatMath.ceil(layout.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(layout.getLineRight(line));
scrollx = right - hspace;
} else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
scrollx = (int) FloatMath.floor(layout.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;
Layout layout = isShowingHint() ? mHintLayout: mLayout;
if (layout == null) return changed;
int line = layout.getLineForOffset(offset);
// FIXME: Is it okay to truncate this, or should we round?
final int x = (int)layout.getPrimaryHorizontal(offset);
final int top = layout.getLineTop(line);
final int bottom = layout.getLineTop(line + 1);
int left = (int) FloatMath.floor(layout.getLineLeft(line));
int right = (int) FloatMath.ceil(layout.getLineRight(line));
int ht = layout.getHeight();
int grav;
switch (layout.getParagraphAlignment(line)) {
case ALIGN_LEFT:
grav = 1;
break;
case ALIGN_RIGHT:
grav = -1;
break;
case ALIGN_NORMAL:
grav = layout.getParagraphDirection(line);
break;
case ALIGN_OPPOSITE:
grav = -layout.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.
// The offsets here are to ensure the rectangle we are using is
// within our view bounds, in case the cursor is on the far left
// or right. If it isn't withing the bounds, then this request
// will be ignored.
if (mTempRect == null) mTempRect = new Rect();
mTempRect.set(x - 2, top, x + 2, 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;
}
int viewportToContentHorizontalOffset() {
return getCompoundPaddingLeft() - mScrollX;
}
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 (mEditor != null &&
(mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
if (singleLine) {
mEditor.mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
} else {
mEditor.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.
*
* If {@link #setMaxLines} has been used to set two or more lines,
* {@link android.text.TextUtils.TruncateAt#END} and
* {@link android.text.TextUtils.TruncateAt#MARQUEE}* are only supported
* (other ellipsizing types will not do anything).
*
* @attr ref android.R.styleable#TextView_ellipsize
*/
public void setEllipsize(TextUtils.TruncateAt where) {
// TruncateAt is an enum. != comparison is ok between these singleton objects.
if (mEllipsize != 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) {
createEditorIfNeeded("setSelectAllOnFocus");
mEditor.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 (visible && mEditor == null) return; // visible is the default value with no edit data
createEditorIfNeeded("setCursorVisible");
if (mEditor.mCursorVisible != visible) {
mEditor.mCursorVisible = visible;
invalidate();
mEditor.makeBlink();
// InsertionPointCursorController depends on mCursorVisible
mEditor.prepareCursorControllers();
}
}
private boolean canMarquee() {
int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
return width > 0 && (mLayout.getLineWidth(0) > width ||
(mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null &&
mSavedMarqueeModeLayout.getLineWidth(0) > width));
}
private void startMarquee() {
// Do not ellipsize EditText
if (getKeyListener() != null) return;
if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
return;
}
if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected()) &&
getLineCount() == 1 && canMarquee()) {
if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
final Layout tmp = mLayout;
mLayout = mSavedMarqueeModeLayout;
mSavedMarqueeModeLayout = tmp;
setHorizontalFadingEdgeEnabled(true);
requestLayout();
invalidate();
}
if (mMarquee == null) mMarquee = new Marquee(this);
mMarquee.start(mMarqueeRepeatLimit);
}
}
private void stopMarquee() {
if (mMarquee != null && !mMarquee.isStopped()) {
mMarquee.stop();
}
if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
final Layout tmp = mSavedMarqueeModeLayout;
mSavedMarqueeModeLayout = mLayout;
mLayout = tmp;
setHorizontalFadingEdgeEnabled(false);
requestLayout();
invalidate();
}
}
private void startStopMarquee(boolean start) {
if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
if (start) {
startMarquee();
} else {
stopMarquee();
}
}
}
/**
* This method is called when the text is changed, in case any subclasses
* would like to know.
*
* Within text
, 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, template pattern method can be overridden by subclasses
}
/**
* 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.
*